最近在写一个小程序,说小也不是很小,麻雀虽小五脏俱全。就是实现C/S模式的打铃,一个TCP服务器和若干客户端,先把客户端的实现说一下,对音频处理不熟悉,走了不少弯路,希望能以此文章帮到有同样需求的。但在以实装为目的编写这个东西之前,需要知道这并不可靠,它不能保证你的每个电脑都开着,即使开着也可能休眠,即使不让休眠也不能保证系统音量大小,即使很容易设置系统音量,也不能保证音箱的音量旋钮位置、音箱开没开…………

一、总体规划

1、服务器(窗体应用):负责分发铃声、实时音频(具有混音功能,但实际根本不用写混音代码就能混音- -!!!)、编制计划等等吧,附加的小功能还一大堆,不一一列举了。

2、客户端(WINDOWS服务):播放铃声音乐、实时音频、调节系统音量等。

二、NUGET

1、NAudio:音频处理、调节系统音量、“混音”等功能

2、WatsonTcp:网络功能

三、服务和服务管理

首先,客户端无窗体、需自启、有足够权限操作输出设备、读写文件……假装有一大堆理由之后,就写个服务呗。

1、创建解决方案——WIN服务(.NET FRAMEROWK 4.7.2)

2、NUGET俩库

3、在解决方案中添加新项目——服务管理窗体

4、在WIM服务中写入代码

    Dim th As TimerProtected Overrides Sub OnStart(ByVal args() As String)' 请在此处添加代码以启动您的服务。此方法应完成设置工作,' 以使您的服务开始工作。TryDim ipstr As String = Registry.LocalMachine.OpenSubKey("SOFTWARE").OpenSubKey("AutoAudio").GetValue("ServerIPPort")'获取服务器IP和PORTDim ipport = ipstr.ToString.Split(":")If ipport.Length = 2 Then'连接服务器TCPClient.Init(ipport(0), CInt(ipport(1)))Elselog("ip err:" & ipstr)End Ifth = New Timer(AddressOf MTimedEvent)th.Change(0, 100)Catch ex As Exceptionlog(ex.ToString)End TryEnd SubProtected Overrides Sub OnStop()On Error Resume NextTCPClient.DisConnect()th.Dispose()End SubPrivate Sub MTimedEvent()TryIf Not TCPClient.Connected ThenTCPClient.Connect()ElseGC.Collect()End If'算了,不锁定播放时音量了,人家有人家的自由Catch ex As Exceptionlog(ex.ToString)End TryEnd SubPrivate Sub log(msg As String)Using stream As FileStream = New FileStream("log.txt", FileMode.Append)Using writer As StreamWriter = New StreamWriter(stream)writer.WriteLine(DateTime.Now.ToString & ":" & msg)End UsingEnd UsingEnd Sub

就是说服务里不让用各种窗体控件,懒得很就用了Thread的Timer。就干一件事:客户端连接。不要迷惑那个注册表,那是服务管理器界面上一个textbox而已。需要注意的是Windows服务编写的时候要尽可能避免错误导致服务终止,但并不是try就行的,那些try是经过充分测试修正了各种可以预见的错误之后添加的——为了防止一些没想到的错误,并且最好有一个log记录功能——这会很好的帮你发现并记录错误。但log也要注意,直接写在当前目录下就可以了(我的是syswow64),也不是每个计算机都有c:d:吧?

5、TPCClient的逻辑


Public Class TCPClientShared MyClient As WatsonTcpClientShared lastConnectTime As DateTimeShared RingPlayer As AudioPlayerShared RecordPlayer As AudioPlayerShared Sub Init(ip As String, port As Integer)RingPlayer = New AudioPlayerRecordPlayer = New AudioPlayerMyClient = New WatsonTcpClient(ip, port)AddHandler MyClient.Events.MessageReceived, AddressOf MessageReceivedAddHandler MyClient.Events.ServerConnected, AddressOf ServerConnectedAddHandler MyClient.Events.ServerDisconnected, AddressOf ServerDisconnectedAddHandler MyClient.Events.ExceptionEncountered, AddressOf ExceptionEncounteredMyClient.Callbacks.SyncRequestReceived = AddressOf SyncRequestReceivedlastConnectTime = Now.AddSeconds(-MyClient.Settings.ConnectTimeoutSeconds * 2)End SubShared Sub Connect()TryDim sp As TimeSpan = Now - lastConnectTimeIf sp.TotalSeconds > MyClient.Settings.ConnectTimeoutSeconds * 2 ThenlastConnectTime = NowMyClient.Connect()End IfCatch ex As ExceptionEnd TryEnd SubShared Sub DisConnect()If MyClient IsNot Nothing Then MyClient.Disconnect()End SubShared Function Connected() As BooleanReturn MyClient IsNot Nothing AndAlso MyClient.ConnectedEnd FunctionPrivate Shared Sub ServerConnected(sender As Object, args As ConnectionEventArgs)End SubPrivate Shared Sub ServerDisconnected(sender As Object, args As DisconnectionEventArgs)End SubPrivate Shared Sub MessageReceived(sender As Object, args As MessageReceivedEventArgs)Select Case args.Metadata("Type").ToStringCase "Play"RingPlayer.ClearBuff()If IO.File.Exists(args.Metadata("Ring")) ThenRingPlayer.WriteProvider(IO.File.ReadAllBytes(args.Metadata("Ring")))Else'这个地方不知道为啥元数据无法到达服务器'Dim timer As New Threading.Timer(New Threading.TimerCallback(AddressOf QRingFile), args.Metadata("Ring"), 0, 0)MyClient.SendAsync(UTF8Encoding.UTF8.GetBytes("RingFile:" & args.Metadata("Ring")))End IfCase "Stop"RingPlayer.ClearBuff()Case "RecordData"RecordPlayer.WriteProvider(args.Data)Case "RingData"IO.File.WriteAllBytes(args.Metadata("FileName"), args.Data)Case "RingDataAndPlay"IO.File.WriteAllBytes(args.Metadata("FileName"), args.Data)RingPlayer.WriteProvider(args.Data)Case "SetRingVolume"RingPlayer.SetVolume(args.Metadata("Volume"))Case "SetRecordVolume"RecordPlayer.SetVolume(args.Metadata("Volume"))End SelectEnd SubPrivate Shared Function SyncRequestReceived(req As SyncRequest) As SyncResponseReturn New SyncResponse(req, req.Data)End FunctionPrivate Shared Sub ExceptionEncountered(sender As Object, args As ExceptionEventArgs)End SubProtected Overrides Sub Finalize()On Error Resume NextMyClient.Dispose()MyBase.Finalize()End Sub
End Class

其实不难理解,就是接到命令之后执行呗:

当接到命令播放时先看看本地有没有对应文件,没有则发一个请求

当接到停止命令时,清空播放缓存,自然就不播放了

当接到实时录音数据时添加到播放录音缓存

当接到铃声数据时写入到文件

当接到铃声数据并播放时接收数据、保存并添加到播放缓存

当接受到设置音量时,设置音量。

唯一值得注意的就是在服务中每隔0.1秒测试一次连接,这明显是短的,如果不需要添加其它功能,那么没有必要这么短,直接设置到比MyClient.Settings.ConnectTimeoutSeconds大就可以了。但规划时我想在这里做一些让客户端讨厌的事情——因为明显可以预见有的客户端会很讨厌的胡捣鼓这个程序。所以,在这里另外用了一个时间判断——这是必要的,一定不要在超时时间内再次连接服务器,否则可能无法正确建立连接。

6、音频播放和混音

首先,“混音”是个没有代码的东西——我写了一些混音程序,也获得了比较好的效果,但后来发现只要每个音频流都对应一个waveOut就可以不用写混音代码:它们会同时、正确的工作,所以,一起播就完了混什么音啊,于是你看到的TCP部分有两个音频播放:


Public Class AudioPlayerConst SampleRate As Integer = 48000         '样本波特率(样本包括转化而来的文件和录音)Const SampleBits As Integer = 16            '样本深度Const SampleChannels As Integer = 1         '样本声道数Dim waveformat As WaveFormat = New WaveFormat(SampleRate, SampleBits, SampleChannels)   '样本音频格式Dim waveProvider As BufferedWaveProvider    '文件缓冲Dim wavOut As WaveOutEventSub New()waveProvider = New BufferedWaveProvider(waveformat)     '文件缓存,读文件时开辟相同大小waveProvider.BufferLength = 1024 * 1024 * 32wavOut = New WaveOutEventwavOut.Init(waveProvider)wavOut.Volume = 1.0wavOut.Play()End SubSub SetVolume(vol As Single)wavOut.Volume = volDim devEnum As MMDeviceEnumerator = New MMDeviceEnumerator()Dim defaultDevice = devEnum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia)defaultDevice.AudioEndpointVolume.MasterVolumeLevelScalar = volEnd SubSub WriteProvider(data() As Byte)waveProvider.AddSamples(data, 0, data.Length)End SubSub ClearBuff()waveProvider.ClearBuffer()End SubFunction GetBuffLen() As IntegerReturn waveProvider.BufferedBytesEnd FunctionProtected Overrides Sub Finalize()wavOut.Stop()wavOut.Dispose()MyBase.Finalize()End SubEnd Class

可以注意到,wavOut一直在工作——这没有任何问题——waveProvider中没有可用数据时是不会发出声音的,所以我们只需要如TCP处理一样只管添加数据就行了。然后是关于系统的播放音量调节,很多文章都指出这需要用API或注册表,但实际上NAudio.CoreAudioApi已经实现了这个东西,在SetVolume函数中首先设置了wavout以指定音量播放,而后通过NAudio.CoreAudioApi来设置系统播放音量——完全可以在系统托盘的音量调节看到效果。类似的,也可以设置麦克风的录制音量。

所以,在写这些代码之前,我很难想象:混音这么容易解决——甚至无需额外代码;调节系统音量只需3行代码(当然不嫌长硬抬杠你可以写一行,不,半行足够)。

7、服务管理程序

这个网上有很多教程,我再总结一下。可以用两种方法去添加app.manifest以取得更高权限从而操作服务:

A、在工程属性——安全性里勾选“启用ClickOnce安全设置”,而后保存再去掉这个勾(不去掉运行不了)

B、手工添加一个应用清单

但无论如何,都需要按文件中给的提示修改requestedExecutionLevel节点:

      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"><!-- UAC 清单选项如果想要更改 Windows 用户帐户控制级别,请使用以下节点之一替换 requestedExecutionLevel 节点。n<requestedExecutionLevel  level="asInvoker" uiAccess="false" /><requestedExecutionLevel  level="requireAdministrator" uiAccess="false" /><requestedExecutionLevel  level="highestAvailable" uiAccess="false" />指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。如果你的应用程序需要此虚拟化来实现向后兼容性,则删除此元素。--><requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />

在WIN7中我没有得到明确的UAC提示,但它确实有效,WIN10中会有提示(选其它凭据运行)。但无论如何如果用管理员权限打开VS,再加载解决方案都是一个好办法。

    Dim serviceFilePath As String = My.Application.Info.DirectoryPath & "/AutoAudio.exe"Dim serviceName As String = "AutoAudio"''' <summary>''' 更新注册表''' </summary>Sub RegUpDate()TryRegistry.LocalMachine.DeleteSubKey("SOFTWARE\AutoAudio", True)Catch ex As ExceptionEnd TryTryDim key = Registry.LocalMachine.OpenSubKey("SOFTWARE", True)Dim subkey = key.CreateSubKey("AutoAudio", True)subkey.SetValue("ServerIPPort", txtServerIPPort.Text, RegistryValueKind.String)Catch ex As ExceptionEnd TryEnd Sub

这是写注册表的部分,当打开上述UAC权限之后,可能你直接运行REGEDIT看不到你写入的键值,这时可以用管理员权限运行或者用读取注册表的代码测试一下取回的是否正确,正确就行了。

PS:服务管理器的其它代码我也是复制粘贴的,自己搜一下吧。记得在启动服务按钮中调用RegUpDate函数以在服务启动前写入注册表,让TCP连接能够知道连接哪个IPPORT(WatsonTcp的IpPort格式是ip:port)

后记:

有时铃声文件的声音很大,而实时录音的音量被掩盖住,所以需要给文件缓冲披上一层外衣,使得wavOut播放的音频流本身的音量可以被调节,这非常简单:

        waveProvider = New BufferedWaveProvider(waveformat)     '文件缓存,读文件时开辟相同大小waveProvider.BufferLength = 1024 * 1024 * 32VolumeProvider = New SampleChannel(waveProvider)VolumeProvider.Volume = 1.0wavOut = New WaveOutEventwavOut.Volume = 1.0wavOut.Init(VolumeProvider)wavOut.Play()

于是,后面可以设置VolumeProvider.Volume的值来使得这个流被播放前音量就发生变化。

网络自动打铃【一】——客户端相关推荐

  1. ONVIF协议网络摄像机(IPC)客户端程序开发(3):理解什么是Web Services

    ONVIF协议网络摄像机(IPC)客户端程序开发(3):理解什么是Web Services 1. 专栏导读 本专栏第一篇文章「专栏开篇」列出了专栏的完整目录,按目录顺序阅读,有助于你的理解,专栏前面文 ...

  2. ONVIF协议网络摄像机(IPC)客户端程序开发(5):门外汉理解ONVIF协议

    1. 专栏导读 本专栏第一篇文章「专栏开篇」列出了专栏的完整目录,按目录顺序阅读,有助于你的理解,专栏前面文章讲过的知识点(或代码段),后面文章不会赘述.为了节省篇幅,突出重点,在文章中展示的示例代码 ...

  3. 腾讯视频怎么开启运营商网络自动播放

    1.打开手机,点击[腾讯视频APP]; 腾讯视频怎么开启运营商网络自动播放 2.进入腾讯视频,点击右下角的[头像标志]; 腾讯视频怎么开启运营商网络自动播放 3.进入腾讯视频菜单,点击底部的[设置]; ...

  4. ONVIF协议网络摄像机(IPC)客户端程序开发(2):第一次使用IPC摄像头

    ONVIF协议网络摄像机(IPC)客户端程序开发(2):第一次使用IPC摄像头 1. 专栏导读 2. 适合读者 本文只适合第一次拿到IPC摄像头,很好奇该怎么使用IPC的读者.已经玩过IPC的,可以略 ...

  5. ONVIF协议网络摄像机(IPC)客户端程序开发(8):获取设备基本信息

    ONVIF协议网络摄像机(IPC)客户端程序开发(8):获取设备基本信息 1 专栏导读 本专栏第一篇文章「专栏开篇」列出了专栏的完整目录,按目录顺序阅读,有助于你的理解,专栏前面文章讲过的知识点(或代 ...

  6. Android 系统(228)---NITZ网络自动对时功能不起作用

    NITZ网络自动对时功能不起作用 在手机选项中开启了自动更新时间/日期,但是时间日期并没有自动更新 Root Cause 1. 网络不支持NITZ功能 2. NITZ信息在PS域(GPRS/EDGE) ...

  7. c语言打铃器单片机程序,基于单片机的自动打铃器的设计

    基于单片机的自动打铃器的设计-电气信息学院毕业设计 目  录 摘要I AbstractII 第一章绪论1 1.1单片机设计的目的和意义1 1.2单片机发展现状和前景展望1 1.2.1课题发展现状1 1 ...

  8. 利用ApacheCXF自动生成webservice的客户端代码

    利用ApacheCXF自动生成webservice的客户端代码 一.环境准备 1.JDK环境 2.下载apache-cxf发布包,举例版本为3.2.14,解压发布包,设置CXF_HOME,并添加%CX ...

  9. esp8266网络自动对时 串口字符连接 病显示 12864i2c u8g2库

    给别人定做的 做的 集成了 烟雾传感器 dht11 u8g2 网络 自动对时 #include <dht11.h>//程序中调用了dht11的库 #include <Arduino. ...

最新文章

  1. Hive常用函数大全一览
  2. 成功解决NameError: name ‘norm‘ is not defined
  3. 电子版,材料获取说明
  4. linux之 !!命令
  5. 上财的计算机专业408,【2020考研】上财408分经验分享
  6. 如何开启并配置CITRIX Xenserver的SNMP服务
  7. Nokia落寞身影下 三星成为全球最大手机厂商
  8. linux服务器系统_利用Zabbix监控系统监测Linux服务器系统时间是否准确完美实现...
  9. 喵喵遇到java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter怎么办
  10. 传相互宝或于6月11日被关停 蚂蚁集团回应:假消息
  11. Python——列表生成式变形
  12. POJ3250(单调栈)
  13. MySQL索引原理及慢查询优化
  14. 如何重设<input type = “file”>
  15. Windows Server 2008 R2之二从介质安装 AD DS
  16. 第一次做web项目购物网站项目总结
  17. 你知道手动探针台系统的用途及组成部分吗?
  18. Android视频融合特效播放与渲染
  19. 安卓期末作品小项目_《去月球》电影版今年上映;电子竞技入选亚室会正式比赛项目...
  20. 亚商投资顾问 早餐FM/0411中证金融下调证券公司保证金比例

热门文章

  1. eclipse中如何使用Git/gitee【雷哥】pull拉代码,commit提交到本地,Push推送到服务器
  2. 音视频7——安卓软编音视频数据推送到rtmp服务器
  3. 查找一段英文中各个单词出现的次数
  4. 从零单排冲kubebuilder(一)
  5. Tornado框架基础
  6. labview传递簇数组到dll
  7. python set类型和set()函数作用、用法等集合,让你学会用set
  8. 国外经纬度计算GPS 计算 google地图计算 必应地图 和mapbox 地图
  9. 如何成为一名真正的黑客
  10. 第四节 基本命令和程序结构控制(1)