自动语音播报系统
与推送系统集成
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Configuration; using System.Threading; using RepairerDispatch.SynthesisServer; using System.Media; using System.IO; using System.Diagnostics; using System.Collections.Concurrent; using System.Text.RegularExpressions; namespace RepairerDispatch { public partial class frmMain : Form { private Counter _Counter = new Counter(); private PushClient _PushClient = null; protected SynchronizationContext SyncContext { get; set; } public ConcurrentQueue<NotifyInfo> NotifyQueue = new ConcurrentQueue<NotifyInfo>(); public bool HandleEnable = false; public int MaxSendCount = int.Parse(ConfigurationManager.AppSettings["RepeatCount"]); public int RepeatInterval = int.Parse(ConfigurationManager.AppSettings["RepeatInterval"]); public bool MockMode = bool.Parse(ConfigurationManager.AppSettings["Mock"]); public bool Quietness = bool.Parse(ConfigurationManager.AppSettings["Quietness"]); public static int FirstSendDelay = int.Parse(ConfigurationManager.AppSettings["Delay"]); public frmMain() { InitializeComponent(); SyncContext = SynchronizationContext.Current; this.MaximizeBox = false; this.StartPosition = FormStartPosition.CenterScreen; #region 配置推送 _PushClient = new PushClient(); _PushClient.UserId = ConfigurationManager.AppSettings["UserId"]; _PushClient.OnPushReceived += this.HandleReceivePush; _PushClient.Start(); #endregion #region 启动消息处理队列 HandleEnable = true; ThreadPool.QueueUserWorkItem(o => { HandleNotifyQueue(); }, null); #endregion #region 统计信息 var timer = new System.Windows.Forms.Timer(); timer.Interval = 100; timer.Tick += (s, e) => { try { lbl1.Text = string.Format("{0}/{1}/{2}", _Counter.Received, _Counter.SendCount, _Counter.CancelCount); lbl2.Text = string.Format("{0}/{1}", _Counter.SynthesisCount, _Counter.SynthesisedCount); lbl3.Text = _Counter.QueueLength.ToString(); } catch { } }; timer.Start(); #endregion } protected override void OnClosing(CancelEventArgs e) { if (MessageBox.Show("关闭窗体后将无法收到提示,确定关闭吗?", "警告!", MessageBoxButtons.YesNo,MessageBoxIcon.Warning) != System.Windows.Forms.DialogResult.Yes) { e.Cancel = true; } base.OnClosing(e); } protected override void OnClosed(EventArgs e) { try { HandleEnable = false; if (_PushClient != null) { _PushClient.OnPushReceived -= this.HandleReceivePush; _PushClient.Dispose(); } } finally { } base.OnClosed(e); } #region 处理推送消息 private void HandleReceivePush(string msg) { if (this.InvokeRequired) { SyncContext.Post((ox) => { HandleReceivePush(msg); }, null); } else { try { Interlocked.Increment(ref _Counter.Received); WriteInfo(msg.Replace(Environment.NewLine,"")); var info= NotifyInfo.Create(msg); if (info == null) return; if (info.VoiceReady) { NotifyQueue.Enqueue(info); _Counter.QueueLength ++; } else { SynthesisVoice(info); } } catch (Exception ex) { WriteInfo(ex.Message); } } } /// <summary> /// 处理队列里的消息 /// </summary> private void HandleNotifyQueue() { try { while (HandleEnable) { //WriteInfo("执行通知队列处理" + DateTime.Now); var info=new NotifyInfo(); _Counter.QueueLength = NotifyQueue.Count; if (NotifyQueue.TryDequeue(out info)) { //符合播放条件 if (info.SendTime <= DateTime.Now ) { #region 播放 using (var ctx = new JL_MFGEntities()) { var it = ctx.L_Conn_CallRepair.FirstOrDefault(ent => ent.ID == info.MsgId); if (it == null || it.TicketStatus != "呼叫" || !string.IsNullOrWhiteSpace(it.TakeOverEmpId)) { if (!MockMode)//模拟模式 { Interlocked.Increment(ref _Counter.CancelCount); continue; } } } Interlocked.Increment(ref _Counter.SendCount); if (!Quietness) { using (SoundPlayer soundPlayer = new SoundPlayer(info.WavFile)) { soundPlayer.Stop(); soundPlayer.PlaySync(); Thread.Sleep(1000 * 6); } } info.SendCount++; info.SendTime = info.SendTime.AddMinutes(RepeatInterval); if (info.SendCount < MaxSendCount) { NotifyQueue.Enqueue(info); } #endregion } else { NotifyQueue.Enqueue(info); } } _Counter.QueueLength = NotifyQueue.Count; if (NotifyQueue.Count <= 0) { Thread.Sleep(450); } Thread.Sleep(50); } } catch (Exception ex) { WriteInfo("错误" + ex.Message); } finally { if (HandleEnable) { ThreadPool.QueueUserWorkItem(o => { HandleNotifyQueue(); }, null); } } } private void SynthesisVoice(NotifyInfo info) { try { #region 合成音频并排队 using (var svr = new SynthesisService()) { WriteInfo(DateTime.Now + ":发起音频合成..." ); Interlocked.Increment(ref _Counter.SynthesisCount); svr.Timeout = 1000 * 360; svr.GetVoiceCompleted += (s, e) => { try { if (_Counter.SynthesisCount > 0)Interlocked.Decrement(ref _Counter.SynthesisCount); if (e.Error != null) throw e.Error; if (e.Cancelled) return; if (e.Result.Code != 0) throw new Exception(e.Result.Msg); var filename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "VCache/" + info.Postion + ".wav"); if(!Directory.Exists(Path.GetDirectoryName( filename))) { Directory.CreateDirectory(Path.GetDirectoryName(filename)); } File.WriteAllBytes(filename, e.Result.Data); info.VoiceReady = true; NotifyQueue.Enqueue(info); Interlocked.Increment(ref _Counter.SynthesisedCount); _Counter.QueueLength ++; WriteInfo(DateTime.Now +":完成音频合成"); } catch (Exception ex) { WriteInfo(ex.Message); } }; svr.GetVoiceAsync(info.Msg, info); } #endregion } catch (Exception ex) { WriteInfo(ex.Message); } } #endregion private void btnTest_Click(object sender, EventArgs e) { try { btnTest.Enabled = false; TestOnlineSynthesis(); #region 保持3秒 for (int i = 0; i < 30; i++) { Thread.Sleep(100); Application.DoEvents(); } #endregion } catch (Exception ex) { MessageBox.Show(ex.Message); } finally { btnTest.Enabled = true; } } private void TestOnlineSynthesis() { Action act = () => { try { #region 在线合成测试 Stopwatch sw = new Stopwatch(); WriteInfo(DateTime.Now +":发起在线合成测试..."); Interlocked.Increment(ref _Counter.SynthesisCount); sw.Start(); var svr = new SynthesisService(); svr.Timeout = 1000 * 360; var msg = ConfigurationManager.AppSettings["TestText"]; var response = svr.GetVoice(msg); //更新计数器 if (_Counter.SynthesisCount > 0) Interlocked.Decrement(ref _Counter.SynthesisCount); if (response.Code != 0) throw new Exception(response.Msg); Interlocked.Increment(ref _Counter.SynthesisedCount); using (MemoryStream ms = new MemoryStream(response.Data)) { using (SoundPlayer soundPlayer = new SoundPlayer(ms)) { soundPlayer.Stop(); soundPlayer.Play(); } } sw.Stop(); WriteInfo(string.Format("{0}:完成在线合成测试,用时{1}毫秒",DateTime.Now,sw.ElapsedMilliseconds)); #endregion } catch (Exception ex) { WriteInfo(ex.Message); } }; var ar = act.BeginInvoke(null, null); } private void WriteInfo(string msg) { if (InvokeRequired) { SyncContext.Post(o => { WriteInfo(msg); }, null); } else { lblTips.Text = msg; Console.WriteLine(msg); } } public class NotifyInfo { public long MsgId { get; set; } public String Postion { get; set; } public String Msg { get; set; } public String WavFile { get; set; } public DateTime AddTime { get; set; } public DateTime SendTime { get; set; } public int SendCount { get; set; } public bool VoiceReady { get; set; } public String EmpName { get; set; } public int EmpId { get; set; } public String Memo { get; set; } public String MachineType { get; set; } #region 机台编号对应表 public static Dictionary<String, String> MachineTypeDic = new Dictionary<string, string>() { {"ZD","自动机"}, {"YJ","压接"}, {"ZJ","中间压接"}, {"DJ","冲床"}, {"XL","电脑剥线机"}, {"QD","扩孔机"}, {"JC","绞缠线"}, {"BJ","包胶机"}, {"RS","热缩机"}, {"YT","一体机"}, {"SC","超声波"} }; #endregion public NotifyInfo() { AddTime = DateTime.Now; SendCount = 0; WavFile = ""; VoiceReady = false; SendTime = DateTime.Now; } public static NotifyInfo Create(String msg) { var info = new NotifyInfo(); var MsgTemp = ConfigurationManager.AppSettings["MsgTemp"]; if (msg.IndexOf("呼叫机修通知") < 0) return null; //呼叫机修通知:($RecId$)\r\n机台:$Position$\r\n员工:$EmpName$($EmpId$)\r\n备注:$Memo$ //呼叫机修通知:(5168) 机台:ZD062 员工:杜多婷(170062) 备注: var m = Regex.Match(msg, @"^呼叫机修通知:(.*?)机台:(.*?)员工:(.*?)备注:(.*?)$", RegexOptions.Singleline| RegexOptions.IgnoreCase); if(!m.Success)return null; info.AddTime = DateTime.Now; info.SendTime = DateTime.Now.AddMinutes( frmMain.FirstSendDelay); info.MsgId =long.Parse( m.Groups[1].Value.Replace("(", "").Replace(")","")); info.Postion = m.Groups[2].Value.Trim().ToUpper(); //员工与工号 var empStr=m.Groups[3].Value; var index=empStr.IndexOf("("); info.EmpName = empStr.Substring(0, index); var endIndex=empStr.IndexOf(")"); info.EmpId= int.Parse(empStr.Substring(index+1, endIndex-index-1)); info.Memo = m.Groups[4].Value.Trim(); var m2 = Regex.Match(info.Postion, @"([a-zA-Z]*)\d*", RegexOptions.Singleline | RegexOptions.IgnoreCase); if (!m2.Success) return null; info.MachineType = m2.Groups[1].Value.Trim(); var mNum = info.Postion.Replace(info.MachineType, "").TrimStart("0".ToCharArray()).Trim(); info.Msg =MsgTemp.Replace("$工序$", MachineTypeDic[info.MachineType]).Replace("$编号$",info.Postion).Replace("$数字$",mNum); var filename= Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "VCache/" + info.Postion + ".wav"); info.WavFile = filename; info.VoiceReady=File.Exists(filename); return info; } } public class Counter { public int SynthesisCount = 0; //正在合成数 public int SynthesisedCount = 0; //已合成数 public int Received = 0; public int SendCount = 0; public int CancelCount = 0; public int QueueLength = 0; } } }
服务端
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Services; using TTS; using System.Text; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Configuration; namespace VoiceSynthesis { /// <summary> /// SynthesisService 的摘要说明 /// </summary> [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem(false)] // 若要允许使用 ASP.NET AJAX 从脚本中调用此 Web 服务,请取消对下行的注释。 // [System.Web.Script.Services.ScriptService] public class SynthesisService : System.Web.Services.WebService { int ret = 0; IntPtr session_ID; [WebMethod] public string HelloWorld() { return "Hello World"; } #region Core /// <summary> /// int>0 声音文件编号 /// 科户端更具这个获取具体名称 /// </summary> /// <param name="msg"></param> /// <returns></returns> [WebMethod] public ConvertResult GetVoice(string msg) { var response = new ConvertResult() { Code = 0, Msg = "", VoiceId = "" }; try { var vid=Path.GetRandomFileName()+".v"; var filename=Server.MapPath("/tmp/"+vid); List<byte> outData; lock (typeof(SynthesisService)) { GenWave(filename, msg,out outData); } response.Data = outData; response.VoiceId = vid; } catch (Exception ex) { response.Code = -1; response.Msg = ex.Message; } return response; } private void GenWave(string fname, string msg,out List<byte> fileBytes) { try { fileBytes = null; ///APPID请勿随意改动 string login_configs = "appid = 57f2ed75xx";//登录参数,自己注册后获取的appid var text = msg; string filename = fname; //合成的语音文件 uint audio_len = 0; SynthStatus synth_status = SynthStatus.MSP_TTS_FLAG_STILL_HAVE_DATA; ret = TTSDll.MSPLogin(string.Empty, string.Empty, login_configs);//第一个参数为用户名,第二个参数为密码,第三个参数是登录参数,用户名和密码需要在http://open.voicecloud.cn //MSPLogin方法返回失败 if (ret != (int)ErrorCode.MSP_SUCCESS) { return; } //string parameter = "engine_type = local, voice_name=xiaoyan, tts_res_path =fo|res\\tts\\xiaoyan.jet;fo|res\\tts\\common.jet, sample_rate = 16000"; //string _params = "ssm=1,ent=sms16k,vcn=xiaoyan,spd=medium,aue=speex-wb;7,vol=x-loud,auf=audio/L16;rate=16000"; //许工,你妈妈喊你回家吃饭 string _params =ConfigurationManager.AppSettings["VSet"] ; //string @params = "engine_type = local,voice_name=xiaoyan,speed=50,volume=50,pitch=50,rcn=1, text_encoding = UTF8, background_sound=1,sample_rate = 16000"; session_ID = TTSDll.QTTSSessionBegin(_params, ref ret); //QTTSSessionBegin方法返回失败 if (ret != (int)ErrorCode.MSP_SUCCESS) { return; } ret = TTSDll.QTTSTextPut(Ptr2Str(session_ID), text, (uint)Encoding.Default.GetByteCount(text), string.Empty); //QTTSTextPut方法返回失败 if (ret != (int)ErrorCode.MSP_SUCCESS) { return; } MemoryStream memoryStream = new MemoryStream(); memoryStream.Write(new byte[44], 0, 44); while (true) { IntPtr source = TTSDll.QTTSAudioGet(Ptr2Str(session_ID), ref audio_len, ref synth_status, ref ret); byte[] array = new byte[(int)audio_len]; if (audio_len > 0) { Marshal.Copy(source, array, 0, (int)audio_len); } memoryStream.Write(array, 0, array.Length); Thread.Sleep(1000); if (synth_status == SynthStatus.MSP_TTS_FLAG_DATA_END || ret != 0) break; } WAVE_Header wave_Header = getWave_Header((int)memoryStream.Length - 44); byte[] array2 = this.StructToBytes(wave_Header); memoryStream.Position = 0L; memoryStream.Write(array2, 0, array2.Length); memoryStream.Position = 0L; //SoundPlayer soundPlayer = new SoundPlayer(memoryStream); //soundPlayer.Stop(); //soundPlayer.Play(); fileBytes = new List<byte>(memoryStream.ToArray()); if (filename != null) { FileStream fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write); memoryStream.WriteTo(fileStream); memoryStream.Close(); fileStream.Close(); } } catch (Exception ) { throw; } finally { ret = TTSDll.QTTSSessionEnd(Ptr2Str(session_ID), ""); ret = TTSDll.MSPLogout();//退出登录 } } /// <summary> /// 结构体转字符串 /// </summary> /// <param name="structure"></param> /// <returns></returns> private byte[] StructToBytes(object structure) { int num = Marshal.SizeOf(structure); IntPtr intPtr = Marshal.AllocHGlobal(num); byte[] result; try { Marshal.StructureToPtr(structure, intPtr, false); byte[] array = new byte[num]; Marshal.Copy(intPtr, array, 0, num); result = array; } finally { Marshal.FreeHGlobal(intPtr); } return result; } /// <summary> /// 结构体初始化赋值 /// </summary> /// <param name="data_len"></param> /// <returns></returns> private WAVE_Header getWave_Header(int data_len) { return new WAVE_Header { RIFF_ID = 1179011410, File_Size = data_len + 36, RIFF_Type = 1163280727, FMT_ID = 544501094, FMT_Size = 16, FMT_Tag = 1, FMT_Channel = 1, FMT_SamplesPerSec = 16000, AvgBytesPerSec = 32000, BlockAlign = 2, BitsPerSample = 16, DATA_ID = 1635017060, DATA_Size = data_len }; } /// <summary> /// 语音音频头 /// </summary> private struct WAVE_Header { public int RIFF_ID; public int File_Size; public int RIFF_Type; public int FMT_ID; public int FMT_Size; public short FMT_Tag; public ushort FMT_Channel; public int FMT_SamplesPerSec; public int AvgBytesPerSec; public ushort BlockAlign; public ushort BitsPerSample; public int DATA_ID; public int DATA_Size; } /// 指针转字符串 /// </summary> /// <param name="p">指向非托管代码字符串的指针</param> /// <returns>返回指针指向的字符串</returns> public static string Ptr2Str(IntPtr p) { List<byte> lb = new List<byte>(); while (Marshal.ReadByte(p) != 0) { lb.Add(Marshal.ReadByte(p)); p = p + 1; } byte[] bs = lb.ToArray(); return Encoding.Default.GetString(lb.ToArray()); } public class ConvertResult { public int Code { get; set; } public String VoiceId { get; set; } public String Msg { get; set; } public List<Byte> Data { get; set; } } #endregion } }
IIS 7.0配置成32位模式,运行在LocalSystem下
<appSettings> <add key="UserId" value="8999001" /> <add key="MsgTemp" value="简讯!{0}:{1}呼叫机修,{0},{1}呼叫机修,完毕!"/> <add key="RepeatCount" value="3"/> <!-- 重复播放次数 --> <add key="RepeatInterval" value="3"/><!-- 重复间隔时间,单位分钟 --> <add key="TestText" value="喜讯!水工,你媳妇又生了,你妈叫你快些子回家,看看生了个啥子!"/> <add key="Mock" value="true"/> </appSettings>
配置