这个问题本质上是关于微软的语音API(SAPI)对于服务器工作负载的适用性以及它是否可以在内部可靠地使用。w3wp用于语音合成。我们有一个异步控制器,它使用本机System.Speech
.NET 4 中的程序集(不是Microsoft.Speech
作为 Microsoft 语音平台 - 运行时版本 11 的一部分提供的一个)和蹩脚.exe 到生成mp3如下:
[CacheFilter]
public void ListenAsync(string url)
{
string fileName = string.Format(@"C:\test\{0}.wav", Guid.NewGuid());
try
{
var t = new System.Threading.Thread(() =>
{
using (SpeechSynthesizer ss = new SpeechSynthesizer())
{
ss.SetOutputToWaveFile(fileName, new SpeechAudioFormatInfo(22050, AudioBitsPerSample.Eight, AudioChannel.Mono));
ss.Speak("Here is a test sentence...");
ss.SetOutputToNull();
ss.Dispose();
}
var process = new Process() { EnableRaisingEvents = true };
process.StartInfo.FileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"bin\lame.exe");
process.StartInfo.Arguments = string.Format("-V2 {0} {1}", fileName, fileName.Replace(".wav", ".mp3"));
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = false;
process.StartInfo.RedirectStandardError = false;
process.Exited += (sender, e) =>
{
System.IO.File.Delete(fileName);
AsyncManager.OutstandingOperations.Decrement();
};
AsyncManager.OutstandingOperations.Increment();
process.Start();
});
t.Start();
t.Join();
}
catch { }
AsyncManager.Parameters["fileName"] = fileName;
}
public FileResult ListenCompleted(string fileName)
{
return base.File(fileName.Replace(".wav", ".mp3"), "audio/mp3");
}
问题是为什么SpeechSynthesizer
需要在这样的单独线程上运行才能返回(这在 SO 的其他地方报告过)here https://stackoverflow.com/questions/4671158/c-sharp-speechsynthesizer-makes-service-unresponsive and here https://stackoverflow.com/questions/1719780/speechsynthesizer-how-do-i-play-save-the-wav-file)以及是否实施STAThreadRouteHandler http://www.productiverage.com/Read/13对于这个请求比上面的方法更有效/可扩展吗?
二、运行时有哪些选项SpeakAsync
在 ASP.NET(MVC 或 WebForms)上下文中?我尝试过的选项似乎都不起作用(请参阅下面的更新)。
欢迎任何有关如何改进此模式的其他建议(即必须彼此串行执行但每个都具有异步支持的两个依赖项)。我认为这个方案在负载下不可持续,特别是考虑到已知内存泄漏 http://connect.microsoft.com/VisualStudio/feedback/details/664196/system-speech-has-a-memory-leak in SpeechSynthesizer
。考虑在不同的堆栈上一起运行此服务。
Update:两者都没有Speak
or SpeakAsnc
选项似乎在以下情况下工作STAThreadRouteHandler
。前者产生:
System.InvalidOperationException:异步操作不是
在这种情况下允许。启动异步操作的页面有
将 Async 属性设置为 true 并执行异步操作
只能在 PreRenderComplete 事件之前的页面上启动。在
System.Web.LegacyAspNetSynchronizationContext.OperationStarted() 位于
System.ComponentModel.AsyncOperationManager.CreateOperation(对象
用户提供的状态)在
System.Speech.Internal.Synthesis.VoiceSynthesis..ctor(弱引用
语音合成器)在
System.Speech.Synthesis.SpeechSynthesizer.get_VoiceSynthesizer() 在
System.Speech.Synthesis.SpeechSynthesizer.SetOutputToWaveFile(字符串
路径、SpeechAudioFormatInfo 格式信息)
后者的结果是:
System.InvalidOperationException:异步操作方法
“Listen”无法同步执行。在
System.Web.Mvc.Async.AsyncActionDescriptor.Execute(ControllerContext
controllerContext、IDictionary`2个参数)
它看起来像是一个自定义的 STA 线程池(带有ThreadStatic
COM 对象的实例)是更好的方法:http://marcinbudny.blogspot.ca/2012/04/dealing-with-sta-coms-in-web.html http://marcinbudny.blogspot.ca/2012/04/dealing-with-sta-coms-in-web.html
更新#2: 好像不是System.Speech.SpeechSynthesizer
需要 STA 处理,只要您遵循这一点,似乎在 MTA 线程上运行良好Start/Join
图案。这是一个新版本,可以正确使用SpeakAsync
(问题是过早地处理它!)并将 WAV 生成和 MP3 生成分成两个单独的请求:
[CacheFilter]
[ActionName("listen-to-text")]
public void ListenToTextAsync(string text)
{
AsyncManager.OutstandingOperations.Increment();
var t = new Thread(() =>
{
SpeechSynthesizer ss = new SpeechSynthesizer();
string fileName = string.Format(@"C:\test\{0}.wav", Guid.NewGuid());
ss.SetOutputToWaveFile(fileName, new SpeechAudioFormatInfo(22050,
AudioBitsPerSample.Eight,
AudioChannel.Mono));
ss.SpeakCompleted += (sender, e) =>
{
ss.SetOutputToNull();
ss.Dispose();
AsyncManager.Parameters["fileName"] = fileName;
AsyncManager.OutstandingOperations.Decrement();
};
CustomPromptBuilder pb = new CustomPromptBuilder(settings.DefaultVoiceName);
pb.AppendParagraphText(text);
ss.SpeakAsync(pb);
});
t.Start();
t.Join();
}
[CacheFilter]
public ActionResult ListenToTextCompleted(string fileName)
{
return RedirectToAction("mp3", new { fileName = fileName });
}
[CacheFilter]
[ActionName("mp3")]
public void Mp3Async(string fileName)
{
var process = new Process()
{
EnableRaisingEvents = true,
StartInfo = new ProcessStartInfo()
{
FileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"bin\lame.exe"),
Arguments = string.Format("-V2 {0} {1}", fileName, fileName.Replace(".wav", ".mp3")),
UseShellExecute = false,
RedirectStandardOutput = false,
RedirectStandardError = false
}
};
process.Exited += (sender, e) =>
{
System.IO.File.Delete(fileName);
AsyncManager.Parameters["fileName"] = fileName;
AsyncManager.OutstandingOperations.Decrement();
};
AsyncManager.OutstandingOperations.Increment();
process.Start();
}
[CacheFilter]
public ActionResult Mp3Completed(string fileName)
{
return base.File(fileName.Replace(".wav", ".mp3"), "audio/mp3");
}