Skip to content

Commit ef3510e

Browse files
committed
修复中国区无法播放的问题,将WebSocket-Sharp改为ClientWebSocket。
1 parent 7dcb484 commit ef3510e

File tree

14 files changed

+2596
-60
lines changed

14 files changed

+2596
-60
lines changed

.vs/Edge_tts_sharp/v17/.suo

23 KB
Binary file not shown.

Edge_tts_sharp/3ohmwvuj.oym~

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
using Edge_tts_sharp.Model;
2+
using System;
3+
using System.Resources;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Net.WebSockets;
8+
using System.Reflection;
9+
using System.Text;
10+
using System.Text.RegularExpressions;
11+
using System.Threading.Tasks;
12+
using Edge_tts_sharp.Utils;
13+
using System.Threading;
14+
using System.Security.Cryptography;
15+
16+
17+
namespace Edge_tts_sharp
18+
{
19+
public class Edge_tts
20+
{
21+
/// <summary>
22+
/// 调试模式
23+
/// </summary>
24+
public static bool Debug = false;
25+
/// <summary>
26+
/// 同步模式
27+
/// </summary>
28+
public static bool Await = false;
29+
30+
private const string ChromiumVersion = "143.0.3650.75";
31+
private const string ChromiumMajorVersion = "143";
32+
33+
private static Dictionary<string, string> Headers { get; } = new Dictionary<string, string>()
34+
{
35+
{ "User-Agent", $"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{ChromiumMajorVersion}.0.0.0 Safari/537.36 Edg/{ChromiumMajorVersion}.0.0.0" },
36+
// { "Accept-Encoding", "gzip, deflate, br, zstd" },
37+
{ "Accept-Language", "en-US,en;q=0.9" },
38+
{ "Pragma", "no-cache" },
39+
{ "Cache-Control", "no-cache" },
40+
{ "Origin", "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold"},
41+
{ "Accept", "*/*" },
42+
};
43+
44+
static string GenerateSecMsGecToken()
45+
{
46+
// 吵了下作业
47+
// 来自 https://github.com/STBBRD/EdgeTTS_dotNET_Framework/
48+
// 来自 https://github.com/rany2/edge-tts/issues/290#issuecomment-2464956570
49+
var ticks = DateTime.Now.ToFileTimeUtc();
50+
ticks -= ticks % 3_000_000_000;
51+
var str = ticks + "6A5AA1D4EAFF4E9FB37E23D68491D6F4";
52+
return ToHexString(HashData(Encoding.ASCII.GetBytes(str)));
53+
}
54+
static string ToHexString(byte[] byteArray)
55+
{
56+
return BitConverter.ToString(byteArray).Replace("-", "").ToUpper();
57+
}
58+
static byte[] HashData(byte[] data)
59+
{
60+
using (SHA256 sha256 = SHA256.Create())
61+
{
62+
byte[] hashBytes = sha256.ComputeHash(data);
63+
return hashBytes;
64+
}
65+
}
66+
static string GetGUID()
67+
{
68+
return Guid.NewGuid().ToString().Replace("-", "");
69+
}
70+
/// <summary>
71+
/// 讲一个浮点型数值转换为百分比数值
72+
/// </summary>
73+
/// <param name="input"></param>
74+
/// <returns></returns>
75+
static string FromatPercentage(double input)
76+
{
77+
string output;
78+
79+
if (input < 0)
80+
{
81+
output = input.ToString("+#;-#;0") + "%";
82+
}
83+
else
84+
{
85+
output = input.ToString("+#;-#;0") + "%";
86+
}
87+
return output;
88+
}
89+
static string ConvertToAudioFormatWebSocketString(string outputformat)
90+
{
91+
return "Content-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n{\"context\":{\"synthesis\":{\"audio\":{\"metadataoptions\":{\"sentenceBoundaryEnabled\":\"false\",\"wordBoundaryEnabled\":\"false\"},\"outputFormat\":\"" + outputformat + "\"}}}}";
92+
}
93+
/// <summary>
94+
///
95+
/// </summary>
96+
/// <param name="lang">输出语言</param>
97+
/// <param name="voice">音源名</param>
98+
/// <param name="rate">语速,-100% - 100% 之间的值,无需传递百分号</param>
99+
/// <param name="text"></param>
100+
/// <returns></returns>
101+
static string ConvertToSsmlText(string lang, string voice, int rate, int volume, string text)
102+
{
103+
return $"<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='{lang}'><voice name='{voice}'><prosody pitch='+0Hz' rate ='{FromatPercentage(rate)}' volume='{volume}'>{text}</prosody></voice></speak>";
104+
}
105+
static string ConvertToSsmlWebSocketString(string requestId, string lang, string voice, int rate, int volume, string msg)
106+
{
107+
return $"X-RequestId:{requestId}\r\nContent-Type:application/ssml+xml\r\nPath:ssml\r\n\r\n{ConvertToSsmlText(lang, voice, rate, volume, msg)}";
108+
}
109+
/// <summary>
110+
/// 语言转文本,将结果返回到回调函数中
111+
/// </summary>
112+
/// <param name="option">播放参数</param>
113+
/// <param name="voice">音源参数</param>
114+
public static void Invoke(PlayOption option, eVoice voice, Action<List<byte>> callback, IProgress<List<byte>> progress = null)
115+
{
116+
var binary_delim = "Path:audio\r\n";
117+
var sendRequestId = GetGUID();
118+
var binary = new List<byte>();
119+
bool IsTurnEnd = false;
120+
121+
var wss = new Wss($"wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4&Sec-MS-GEC={GenerateSecMsGecToken()}&Sec-MS-GEC-Version={ChromiumVersion}");
122+
123+
wss.AddCookie("muid", GetGUID(), "/", "speech.platform.bing.com");
124+
foreach (var header in Headers)
125+
{
126+
wss.AddHeader(header.Key, header.Value);
127+
}
128+
129+
wss.OnMessage += (sender, e) =>
130+
{
131+
if (e.IsText)
132+
{
133+
var data = e.Data;
134+
var requestId = Regex.Match(data, @"X-RequestId:(?<requestId>.*?)\r\n").Groups["requestId"].Value;
135+
if (data.Contains("Path:turn.start"))
136+
{
137+
// start of turn, ignore. 开始信号,不用处理
138+
}
139+
else if (data.Contains("Path:turn.end"))
140+
{
141+
// 返回内容
142+
if (binary.Count > 0)
143+
{
144+
callback?.Invoke(binary);
145+
}
146+
else
147+
{
148+
throw new Exception("返回值为空!");
149+
}
150+
// end of turn, close stream. 结束信号,可主动关闭socket
151+
// 音频发送完毕后,最后还会收到一个表示音频结束的文本信息
152+
//wss.Close();
153+
}
154+
else if (data.Contains("Path:response"))
155+
{
156+
// context response, ignore. 响应信号,无需处理
157+
}
158+
else
159+
{
160+
// 未知错误,通常不会发生
161+
}
162+
if (Debug) Console.WriteLine(e.Data);
163+
IsTurnEnd = true;
164+
}
165+
else if (e.IsBinary)
166+
{
167+
var data = e.RawData;
168+
var requestId = Regex.Match(e.Data, @"X-RequestId:(?<requestId>.*?)\r\n").Groups["requestId"].Value;
169+
if (data[0] == 0x00 && data[1] == 0x67 && data[2] == 0x58)
170+
{
171+
// Last (empty) audio fragment. 空音频片段,代表音频发送结束
172+
}
173+
else
174+
{
175+
var index = Encoding.UTF8.GetString(data).IndexOf(binary_delim) + binary_delim.Length;
176+
var curVal = data.Skip(index);
177+
binary.AddRange(curVal);
178+
// 传出
179+
progress?.Report(curVal.ToList());
180+
}
181+
}
182+
};
183+
wss.OnColse += (sender, e) =>
184+
{
185+
if (!string.IsNullOrEmpty(option.SavePath))
186+
{
187+
File.WriteAllBytes(option.SavePath, binary.ToArray());
188+
}
189+
};
190+
wss.OnLog += (onmsg) =>
191+
{
192+
if (Debug) Console.WriteLine($"[{onmsg.level.ToString()}] {onmsg.msg}");
193+
};
194+
if (wss.Run())
195+
{
196+
wss.Send(ConvertToAudioFormatWebSocketString(voice.SuggestedCodec));
197+
wss.Send(ConvertToSsmlWebSocketString(sendRequestId, voice.Locale, voice.Name, option.Rate, ((int)option.Volume * 100), option.Text));
198+
}
199+
while (Await && !IsTurnEnd)
200+
{
201+
Thread.Sleep(10);
202+
}
203+
}
204+
/// <summary>
205+
/// 另存为mp3文件
206+
/// </summary>
207+
/// <param name="option">播放参数</param>
208+
/// <param name="voice">音源参数</param>
209+
public static void SaveAudio(PlayOption option, eVoice voice)
210+
{
211+
if (string.IsNullOrEmpty(option.SavePath))
212+
{
213+
throw new Exception("保存路径为空,请核对参数后重试.");
214+
}
215+
Invoke(option, voice, null);
216+
}
217+
/// <summary>
218+
/// 调用微软Edge接口,文字转语音
219+
/// </summary>
220+
/// <param name="option">播放参数</param>
221+
/// <param name="voice">音源参数</param>
222+
public static void PlayText(PlayOption option, eVoice voice)
223+
{
224+
Invoke(option, voice, (_binary) =>
225+
{
226+
Audio.PlayToByteAsync(_binary.ToArray(), option.Volume);
227+
});
228+
}
229+
/// <summary>
230+
/// 获取一个`AudioPlayer`的对象
231+
/// </summary>
232+
/// <param name="option">播放参数</param>
233+
/// <param name="voice">音源参数</param>
234+
/// <returns></returns>
235+
public static AudioPlayer GetPlayer(PlayOption option, eVoice voice)
236+
{
237+
AudioPlayer player = null;
238+
Invoke(option, voice, (_binary) =>
239+
{
240+
player = new AudioPlayer(_binary.ToArray(), option.Volume);
241+
});
242+
while (player == null)
243+
{
244+
Thread.Sleep(10);
245+
}
246+
return player;
247+
}
248+
/// <summary>
249+
/// 同步等待播放音频结束
250+
/// </summary>
251+
/// <param name="option">播放参数</param>
252+
/// <param name="voice">音源参数</param>
253+
//public static void PlayTextAsync(PlayOption option, eVoice voice)
254+
//{
255+
// List<byte> buffer = new List<byte>();
256+
// var audioStreamer = new Mp3AudioStreamer();
257+
// var report = new Progress<List<byte>>((binary) =>
258+
// {
259+
// audioStreamer.OnAudioReceived(binary.ToArray());
260+
// });
261+
// Invoke(option, voice, null, report);
262+
263+
// audioStreamer.Stop();
264+
//}
265+
266+
/// <summary>
267+
/// 获取支持的音频列表
268+
/// </summary>
269+
/// <returns></returns>
270+
public static List<eVoice> GetVoice()
271+
{
272+
var voiceList = Tools.GetEmbedText("Edge_tts_sharp.Source.VoiceList.json");
273+
return Tools.StringToJson<List<eVoice>>(voiceList);
274+
}
275+
}
276+
}

0 commit comments

Comments
 (0)