处于项目需要,我研究了一下web端的语音识别实现。目前市场上语音服务已经非常成熟了,国内的科大讯飞或是国外的微软在这块都可以提供足够优质的服务,对于我们工程应用来说只需要花钱调用接口就行了,难点在于整体web应用的开发。最开始我实现了一个web端录好音然后上传服务端进行语音识别的简单demo,但是这种结构太过简单,对浏览器的负担太重,而且响应慢,交互差;后来经过调研,发现微软的语音服务接口是支持流输入的连续识别的,因此开发重点就在于实现前后端的流式传输。
参考这位国外大牛写的博文Continuous speech to text on the server with Cognitive Services,开发的新demo可以达到理想的效果,在网页上点击“开始录音”开启一次录音,对着麦克风随意说话,网页会把实时的音频数据传递给后端,后端实时识别并返回转换结果,点击“结束录音”停止。

1 整体结构
整体结构图如下所示,在web端需要使用HTML5的Web Audio API接收麦克风输入的音频流,进行适量的处理后实时传递给服务端;web与服务端之间的音频流交互通过SignalR来实现;具体的语音识别通过调用微软语音服务实现。
该web实时语音识别demo可以实现下面的功能:

可以通过网页传入麦克风音频
网页可以实时显示语音识别结果,包括中间结果和最终结果
可以保存每一次的录音,并且录音时长可以非常长
支持多个web同时访问,服务端管理多个连接
2 技术栈
ASP.NET Core开发
服务端使用较新的 ASP.NET Core技术开发,不同于传统的 ASP.NET,ASP.NET Core更加适合前后台分离的web应用,我们会用 ASP.NET Core框架开发REST API为前端服务。如果不去纠结 ASP.NET Core的框架结构,实际的开发和之前的.NET应用开发没什么不同,毕竟只是底层结构不同。
JavaScript或TypeScript开发
前端的逻辑用JavaScript开发,具体什么框架无所谓。我是用angular开发的,因此严格的说开发语言是TypeScript了。至于网页的具体内容,熟悉html、CSS就行了。
Web Audio API的使用和基本的音频处理的知识
采集和上传麦克风音频都在浏览器进行,对此需要使用HTML5标准下的Web API进行音频流的获取,使用音频上下文(AudioContext)实时处理音频流。具体处理音频流时,需要了解一点基本的音频知识,例如采样率、声道等参数,WAV文件格式等等。
相关资料:
HTML5网页录音和压缩
HTML5 getUserMedia/AudioContext 打造音谱图形化
Capturing Audio & Video in HTML5
微软语音认知服务
微软的语音识别技术是微软云服务中的成员之一,相比于国内比较熟知的科大讯飞,微软的优势在于契合 .NET Core技术栈,开发起来非常方便,支持连续识别,支持自定义训练,并且支持容器部署,这对于那些对上传云服务有安全顾虑的用户更是好消息。当然价格考虑就得看具体情况了,不过如果你有Azure账号的话,可以开通标准版本的语音识别服务,这是免费的,只有时间限制;没有账号的话可以使用微软提供的体验账号体验一个月。
官方文档: Speech Services Documentation
SignalR的使用
要实现web和服务端的流通信,就必须使用web socket一类的技术来进行长连接通信,微软的SignalR是基于web socket的实时通信技术,如果我们的web需要和服务端保持长连接或者需要接收服务端的消息推送,使用该技术可以方便的实现。需要注意的是 ASP.NET SignalR and ASP.NET Core SignalR是有区别的,在 .NET Core环境下需要导入的是SignalR Core
官方文档: Introduction to ASP.NET Core SignalR
Angular下调用SignalR: How to Use SignalR with .NET Core and Angular
字节流和异步编程的概念
参考这篇博文Continuous speech to text on the server with Cognitive Services,在服务端需要自己实现一个特别的字节流来作为语音服务的数据源,因为语音服务在默认的字节流上一旦读取不到数据就会自动结束。在具体的实现中,将会用到一些信号量来进行读取控制。
3 后端细节
3.1 获取微软语音识别服务
如果没有Azure账号,可以用微软提供的试用账号:

有Azure账号的话,在Azure门户里开通语音识别服务

创建的时候可以选择F0类型的收费标准,这种是免费的:

开通成功后,得到API Key的值,我们调用服务的时候传入这个参数;另一个参数是region,这个要看你创建服务的时候选择的区域,如果你选择的是East Asia,这个参数就是“eastasia”,如果用的是测试账号,统一用“westus”。

3.2 创建并配置ASP.NET Core API项目
新建一个 ASP.NET Core API项目

通过Nuget管理器添加语音服务和SignalR相关的包。Microsoft.CognitiveServices.Speech是微软语音服务包,Microsoft.AspNetCore.SignalR.Protocols.MessagePack用于SignalR中的MessagePack协议通信。

在Startup.cs中为 ASP.NET Core项目注入并配置SignalR服务:

public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.WithOrigins("http://localhost:4200")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
}); // 跨域请求设置
services.AddSignalR().AddMessagePackProtocol(options =>
{
options.FormatterResolvers = new List<MessagePack.IFormatterResolver>()
{
MessagePack.Resolvers.StandardResolver.Instance
};
}); // 允许signalR以MessagePack消息进行通信
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseCors("CorsPolicy"); //添加跨域请求服务
app.UseSignalR(routes =>
{
routes.MapHub<ContinuousS2TAPI.S2THub.S2THub>("/s2thub");
}); //添加SignalR服务并配置路由,访问'/s2thub'将被映射到S2THub对象上
app.UseMvc();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
3.3 SignalR接口
新建一个S2THub文件夹,将SignalR接口放着里面。先创建一个Connection类,它代表一个客户端连接:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CognitiveServices.Speech;
using ContinuousS2TAPI.Speech;

namespace ContinuousS2TAPI.S2THub
{
public class Connection
{
public string SessionId; // 会话ID
public SpeechRecognizer SpeechClient; // 一个语音服务对象
public VoiceAudioStream AudioStream; //代表一个音频流
public List<byte> VoiceData; //存储该次会话的音频数据
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后创建继承自Hub的S2THub类,这个实例化的S2THub对象将会管理客户端的连接

public class S2THub : Hub
{
private static IConfiguration _config;
private static IHubContext<S2THub> _hubContext; //S2THub实例的上下文
private static Dictionary<string, Connection> _connections; //维护客户端连接

public S2THub(IConfiguration configuration, IHubContext<S2THub> ctx)
{
if (_config == null)
_config = configuration;

if (_connections == null)
_connections = new Dictionary<string, Connection>();

if (_hubContext == null)
_hubContext = ctx;
}
...
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在S2THub类中,需要定义两个供客户端调用的接口AudioStart和ReceiveAudio,客户端首先需要通过调用AudioStart来通知服务端开始一次语音识别会话,接着在接收到实时的音频二进制数据后调用ReceiveAudio接口来将数据发送给服务端。这里会用到一个名为VoiceAudioStream的流对象,这是我们自定义的流对象,具体实现后文给出。

public async void AudioStart()
{
Console.WriteLine($"Connection {Context.ConnectionId} starting audio.");
var audioStream = new VoiceAudioStream(); // 创建一个供语音识别对象读取的数据流
var audioFormat = AudioStreamFormat.GetWaveFormatPCM(16000, 16, 1);
var audioConfig = AudioConfig.FromStreamInput(audioStream, audioFormat);
var speechConfig = SpeechConfig.FromSubscription("your key", "you region"); //使用你自己的key和region参数
speechConfig.SpeechRecognitionLanguage = "zh-CN"; //中文

var speechClient = new SpeechRecognizer(speechConfig, audioConfig);

speechClient.Recognized += _speechClient_Recognized; // 连续识别存在Recognized和Recognizing事件
speechClient.Recognizing += _speechClient_Recognizing;
speechClient.Canceled += _speechClient_Canceled;

string sessionId = speechClient.Properties.GetProperty(PropertyId.Speech_SessionId);

var conn = new Connection()
{
SessionId = sessionId,
AudioStream = audioStream,
SpeechClient = speechClient,
VoiceData = new List<byte>()
};

_connections.Add(Context.ConnectionId, conn); //将这个新的连接记录

await speechClient.StartContinuousRecognitionAsync(); //开始连续识别

Console.WriteLine("Audio start message.");

}

public void ReceiveAudio(byte[] audioChunk)
{
//Console.WriteLine("Got chunk: " + audioChunk.Length);
_connections[Context.ConnectionId].VoiceData.AddRange(audioChunk); //记录接收到的音频数据
_connections[Context.ConnectionId].AudioStream.Write(audioChunk, 0, audioChunk.Length);//并将实时的音频数据写入流
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
要将识别的结果返回给客户端,就需要调用客户端的接口实现推送,因此定义一个SendTranscript方法,内部会调用客户端名为IncomingTranscript的接口来推送消息:

public async Task SendTranscript(string text, string sessionId)
{
var connection = _connections.Where(c => c.Value.SessionId == sessionId).FirstOrDefault();
await _hubContext.Clients.Client(connection.Key).SendAsync("IncomingTranscript", text); //调用指定客户端的IncomingTranscript接口
}
1
2
3
4
5
在语音服务的识别事件中我们调用SendTranscript方法返回结果:

private async void _speechClient_Canceled(object sender, SpeechRecognitionCanceledEventArgs e)
{
Console.WriteLine("Recognition was cancelled.");
if (e.Reason == CancellationReason.Error)
{
Console.WriteLine($"CANCELED: ErrorCode={e.ErrorCode}");
Console.WriteLine($"CANCELED: ErrorDetails={e.ErrorDetails}");
Console.WriteLine($"CANCELED: Did you update the subscription info?");
await SendTranscript("识别失败!", e.SessionId);
}
}

private async void _speechClient_Recognizing(object sender, SpeechRecognitionEventArgs e)
{
Console.WriteLine($"{e.SessionId} > Intermediate result: {e.Result.Text}");
await SendTranscript(e.Result.Text, e.SessionId);
}

private async void _speechClient_Recognized(object sender, SpeechRecognitionEventArgs e)
{
Console.WriteLine($"{e.SessionId} > Final result: {e.Result.Text}");
await SendTranscript(e.Result.Text, e.SessionId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
最后我们重写Hub的OnDisconnectedAsync方法,这个方法会在hub连接断开时调用,可以在该方法内结束识别:

public async override Task OnDisconnectedAsync(Exception exception)
{
var connection = _connections[Context.ConnectionId];
Console.WriteLine($"Voice list length : {connection.VoiceData.Count}");
byte[] actualLength = System.BitConverter.GetBytes(connection.VoiceData.Count);
string rootDir = AppContext.BaseDirectory;
System.IO.DirectoryInfo directoryInfo = System.IO.Directory.GetParent(rootDir);
string root = directoryInfo.Parent.Parent.FullName;
var savePath = $"{root}\\voice{connection.SessionId}.wav";
using (var stream = new System.IO.FileStream(savePath, System.IO.FileMode.Create)) // 保存音频文件
{
byte[] bytes = connection.VoiceData.ToArray();
bytes[4] = actualLength[0];
bytes[5] = actualLength[1];
bytes[6] = actualLength[2];
bytes[7] = actualLength[3];
bytes[40] = actualLength[0];
bytes[41] = actualLength[1];
bytes[42] = actualLength[2];
bytes[43] = actualLength[3]; // 计算并填入音频数据长度
stream.Write(bytes, 0, bytes.Length);
}

await connection.SpeechClient.StopContinuousRecognitionAsync(); //结束识别
connection.SpeechClient.Dispose();
connection.AudioStream.Dispose();
_connections.Remove(Context.ConnectionId); //移除连接记录
Console.WriteLine($"connection : {Context.ConnectionId} closed");
await base.OnDisconnectedAsync(exception);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
3.4 自定义音频流
SpeechRecognizer对象可以接收一个特殊的流对象PullAudioStreamCallback作为数据源,如果传入了这个对象,SpeechRecognizer会主动从该流对象里读取数据。这个一个虚类,如果你只是给一段音频文件做识别,通过MemoryStream和BinaryStreamReader的简单组合就可以了(可以看微软的官方demo),但是SpeechRecognizer会在流中读取到0个字节后停止识别,在我们的场景中默认的流类型无法满足需求,当没有数据读取到时它们无法block住,PullAudioStreamCallback期望的效果是只有当明确流结束时读取流的Read()方法才返回0。因此需要定义我们自己的音频流对象
首先定义一个继承自MemoryStream的EchoStream, 该流对象会在没有数据进入时进行等待而不是直接返回0

public class EchoStream:MemoryStream
{
private readonly ManualResetEvent _DataReady = new ManualResetEvent(false);
private readonly ConcurrentQueue<byte[]> _Buffers = new ConcurrentQueue<byte[]>();

public bool DataAvailable { get { return !_Buffers.IsEmpty; } }

public override void Write(byte[] buffer, int offset, int count)
{
_Buffers.Enqueue(buffer.Take(count).ToArray());
_DataReady.Set();
}

public override int Read(byte[] buffer, int offset, int count)
{
//Debug.WriteLine("Data available: " + DataAvailable);

_DataReady.WaitOne();

byte[] lBuffer;

if (!_Buffers.TryDequeue(out lBuffer))
{
_DataReady.Reset();
return -1;
}

if (!DataAvailable)
_DataReady.Reset();

Array.Copy(lBuffer, buffer, lBuffer.Length);
return lBuffer.Length;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
然后定义PullAudioStreamCallback对象,作为语音服务的输入源。服务端会把客户端上传的byte[]数据通过Write()方法写入流,而语音服务会调用Read()方法读取数据,可以看到,通过一个ManualResetEvent信号量,使得流对象必须在调用Close()方法之后才会在Read()方法中返回0

public class VoiceAudioStream : PullAudioInputStreamCallback
{
private readonly EchoStream _dataStream = new EchoStream();
private ManualResetEvent _waitForEmptyDataStream = null;

public override int Read(byte[] dataBuffer, uint size) //S2T服务从PullAudioInputStream中读取数据, 读到0个字节并不会关闭流
{
if (_waitForEmptyDataStream != null && !_dataStream.DataAvailable)
{
_waitForEmptyDataStream.Set();
return 0;
}

return _dataStream.Read(dataBuffer, 0, dataBuffer.Length);
}

public void Write(byte[] buffer, int offset, int count) //Client向PullAudioInputStream写入数据
{
_dataStream.Write(buffer, offset, count);
}

public override void Close()
{
if (_dataStream.DataAvailable)
{
_waitForEmptyDataStream = new ManualResetEvent(false); //通过ManualResetEvent强制流的使用者必须调用close来手动关闭流
_waitForEmptyDataStream.WaitOne();
}

_waitForEmptyDataStream.Close();
_dataStream.Dispose();
base.Close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
4 前端细节
前端是用Angular框架写的,这个Demo我们需要用到SignalR相关的库。使用npm install @aspnet/signalr和npm install @aspnet/signalr-protocol-msgpack安装这两个包,并导入到组件中:

此外在polyfills.ts文件中添加如下代码,否则可能会有浏览器兼容问题

在组件的初始化代码中,初始化Hub对象,并检查当前浏览器是否支持实时音频,目前主要是firefox和chrome支持浏览器流媒体获取。通过this.s2tHub.on('IncomingTranscript', (message) => { this.addMessage(message); });这句代码,给客户端注册了一个IncomingTranscript方法以供服务端调用

constructor(private http: HttpClient) {
this.s2tHub = new signalR.HubConnectionBuilder()
.withUrl(this.apiurl) //apiurl是SignalR Hub的地址,我这里是'https://localhost:5001/s2thub'
.withHubProtocol(new signalRmsgpack.MessagePackHubProtocol())
.configureLogging(signalR.LogLevel.Information)
.build(); // 创建Hub对象
this.s2tHub.on('IncomingTranscript', (message) => {
this.addMessage(message);
}); // 在客户端注册IncomingTranscript接口
}

ngOnInit() {
if (navigator.mediaDevices.getUserMedia) { // 检测当前浏览器是否支持流媒体
this.addMessage('This browser support getUserMedia');
this.supportS2T = true;
} else {
this.addMessage('This browser does\'nt support getUserMedia');
this.supportS2T = false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
点击"Start"后,启动Hub连接,开始向服务端发送数据。先是调用服务端的AudioStart接口通知服务端开始识别;然后调用ReceiveAudio接口先向服务端发送44字节的wav头数据,语音识别服务目前支持的音频类型是PCM和WAV,16000采样率,单声道,16位宽;最后在startStreaming中开始处理实时音频流。

async startRecord() {
if (!this.supportS2T) {
return;
}
let connectSuccess = true;
if (this.s2tHub.state !== signalR.HubConnectionState.Connected) {
await this.s2tHub.start().catch(err => { this.addMessage(err); connectSuccess = false; } );
}
if (!connectSuccess)
{
return;
}

this.s2tHub.send('AudioStart');
this.s2tHub.send('ReceiveAudio', new Uint8Array(this.createStreamRiffHeader()));
this.startStreaming();
}

private createStreamRiffHeader() {
// create data buffer
const buffer = new ArrayBuffer(44);
const view = new DataView(buffer);

/* RIFF identifier */
view.setUint8(0, 'R'.charCodeAt(0));
view.setUint8(1, 'I'.charCodeAt(0));
view.setUint8(2, 'F'.charCodeAt(0));
view.setUint8(3, 'F'.charCodeAt(0));

/* file length */
view.setUint32(4, 0, true); // 因为不知道数据会有多长,先将其设为0
/* RIFF type & Format */
view.setUint8(8, 'W'.charCodeAt(0));
view.setUint8(9, 'A'.charCodeAt(0));
view.setUint8(10, 'V'.charCodeAt(0));
view.setUint8(11, 'E'.charCodeAt(0));
view.setUint8(12, 'f'.charCodeAt(0));
view.setUint8(13, 'm'.charCodeAt(0));
view.setUint8(14, 't'.charCodeAt(0));
view.setUint8(15, ' '.charCodeAt(0));

/* format chunk length */
view.setUint32(16, 16, true); // 16位宽
/* sample format (raw) */
view.setUint16(20, 1, true);
/* channel count */
view.setUint16(22, 1, true); // 单通道
/* sample rate */
view.setUint32(24, 16000, true); // 16000采样率
/* byte rate (sample rate * block align) */
view.setUint32(28, 32000, true);
/* block align (channel count * bytes per sample) */
view.setUint16(32, 2, true);
/* bits per sample */
view.setUint16(34, 16, true);
/* data chunk identifier */
view.setUint8(36, 'd'.charCodeAt(0));
view.setUint8(37, 'a'.charCodeAt(0));
view.setUint8(38, 't'.charCodeAt(0));
view.setUint8(39, 'a'.charCodeAt(0));

/* data chunk length */
view.setUint32(40, 0, true); // 因为不知道数据会有多长,先将其设为0

return buffer;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
在startStreaming方法中,首先调用window.navigator.mediaDevices.getUserMedia获取到麦克风输入流,然后创建AudioContext对象,用它创建多个音频处理节点,这些节点依次连接:

audioInput节点是音频输入节点,以音频流为输入;lowpassFilter节点作为一个滤波节点,对输入音频做低通滤波进行简单的降噪;jsScriptNode节点是主要的处理节点,可以为该节点添加事件处理,每当有数据进入该节点就进行处理和上传;最后是destination节点,它是Web Audio Context终结点,默认情况下会连接到本地的扬声器。

private startStreaming() {
window.navigator.mediaDevices.getUserMedia({
audio: true
}).then(mediaStream => {
this.addMessage('get media stream successfully');
this.audioStream = mediaStream;
this.context = new AudioContext();

this.audioInput = this.context.createMediaStreamSource(this.audioStream); // 源节点

this.lowpassFilter = this.context.createBiquadFilter();
this.lowpassFilter.type = 'lowpass';
this.lowpassFilter.frequency.setValueAtTime(8000, this.context.currentTime); //滤波节点

this.jsScriptNode = this.context.createScriptProcessor(4096, 1, 1);
this.jsScriptNode.addEventListener('audioprocess', event => {
this.processAudio(event);
}); // 处理事件

this.audioInput.connect(this.lowpassFilter);
this.lowpassFilter.connect(this.jsScriptNode);
this.jsScriptNode.connect(this.context.destination);

}).catch(err => {
this.addMessage('get media stream failed');
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
在jsScriptNode节点的audioprocess事件中,我们通过降采样、取单通道音频等处理获得正确格式的音频数据,再调用服务端的ReceiveAudio方法上传数据块

private processAudio(audioProcessingEvent: any) {
var inputBuffer = audioProcessingEvent.inputBuffer;
// The output buffer contains the samples that will be modified and played
var outputBuffer = audioProcessingEvent.outputBuffer;
var isampleRate = inputBuffer.sampleRate;
var osampleRate = 16000;
var inputData = inputBuffer.getChannelData(0);
var outputData = outputBuffer.getChannelData(0);
var output = this.downsampleArray(isampleRate, osampleRate, inputData);

for (var i = 0; i < outputBuffer.length; i++) {
outputData[i] = inputData[i];
}

this.s2tHub.send('ReceiveAudio', new Uint8Array(output.buffer)).catch(err => {this.addMessage(err); });
}

private downsampleArray(irate: any, orate: any, input: any): Int16Array { // 降采样
const ratio = irate / orate;
const olength = Math.round(input.length / ratio);
const output = new Int16Array(olength);

var iidx = 0;
var oidx = 0;

for (var oidx = 0; oidx < output.length; oidx++) {
const nextiidx = Math.round((oidx + 1) * ratio);

var sum = 0;
var cnt = 0;

for (; iidx < nextiidx && iidx < input.length; iidx++) {
sum += input[iidx];
cnt++;
}

// saturate output between -1 and 1
var newfval = Math.max(-1, Math.min(sum / cnt, 1));

// multiply negative values by 2^15 and positive by 2^15 -1 (range of short)
var newsval = newfval < 0 ? newfval * 0x8000 : newfval * 0x7FFF;

output[oidx] = Math.round(newsval);
}

return output;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
最后,定义stopRecord方法,断开AudioContext连接和Hub连接:

async stopRecord() {
this.jsScriptNode.disconnect(this.context.destination);
this.lowpassFilter.disconnect(this.jsScriptNode);
this.audioInput.disconnect(this.lowpassFilter);
this.s2tHub.stop();
}
1
2
3
4
5
6
5 扩展
自定义语音模型
可以上传训练数据进行自定义模型的训练,得到的自定义语音服务可以适应更加特定的业务场景。 不过自定义模型是需要开通标准收费服务的
容器部署
微软目前也支持服务的容器部署,通过服务的本地部署,可以提高信息传输速度,并且减小私有信息安全性的顾虑

————————————————
版权声明:本文为CSDN博主「皇家园林巡游者」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wangzhenyang2/article/details/96020000

ASP.NET Core环境Web Audio API+SingalR+微软语音服务实现web实时语音识别的更多相关文章

  1. Web Audio API之手把手教你用web api处理声音信号:可视化音乐demo

    1.Web Audio API 介绍 Web Audio API 提供了在Web上控制音频的一个非常有效通用的系统 ,这些通用系统通俗的讲就是我们可以利用Web Audio API提供的各种方法操作各 ...

  2. 【HTML5】Web Audio API打造超炫的音乐可视化效果

    HTML5真是太多炫酷的东西了,其中Web Audio API算一个,琢磨着弄了个音乐可视化的demo,先上效果图: 项目演示:别说话,点我!  源码已经挂到github上了,有兴趣的同学也可以去st ...

  3. 在Linux(Ubuntu)下搭建ASP.NET Core环境并运行 继续跨平台

    最新教程:http://www.cnblogs.com/linezero/p/aspnetcoreubuntu.html 无需安装mono,在Linux(Ubuntu)下搭建ASP.NET Core环 ...

  4. ASP.NET Core环境并运行 继续跨平台

    ASP.NET Core环境并运行 继续跨平台 无需安装mono,在Linux(Ubuntu)下搭建ASP.NET Core环境 继续.NET跨平台 上一篇:使用VS Code开发ASP.NET Co ...

  5. HTML5 ——web audio API 音乐可视化(一)

    使用Web Audio API可以对音频进行分析和操作,最终实现一个音频可视化程序. 最终效果请戳这里; 完整版代码请戳这里,如果还看得过眼,请给一个start⭐ 一.API AudioContext ...

  6. 【转】asp.net core环境变量详解

    asp.net core环境变量详解 环境变量详解 Windows操作系统的环境变量在哪设置应该都知道了. Linux(centos版本)的环境变量在/etc/profile里面进行设置.用户级的环境 ...

  7. ASP.NET Core应用针对静态文件请求的处理[1]: 以Web的形式发布静态文件

    虽然ASP.NET Core是一款"动态"的Web服务端框架,但是在很多情况下都需要处理针对静态文件的请求,最为常见的就是这对JavaScript脚本文件.CSS样式文件和图片文件 ...

  8. 关于HTML5音频——audio标签和Web Audio API各平台浏览器的支持情况

    对比audio标签 和 Web Audio API 各平台浏览器的支持情况:   audio element Web Audio API desktop browsers Chrome 14 Yes  ...

  9. ASP.NET CORE 1.0 MVC API 文档用 SWASHBUCKLE SWAGGER实现

    from:https://damienbod.com/2015/12/13/asp-net-5-mvc-6-api-documentation-using-swagger/ 代码生成工具: https ...

  10. [Javascript] Intro to the Web Audio API

    An introduction to the Web Audio API. In this lesson, we cover creating an audio context and an osci ...

随机推荐

  1. Spring Cloud Sleuth:分布式请求链路跟踪

    Spring Cloud Sleuth:分布式请求链路跟踪 SpringCloud学习教程 SpringCloud Spring Cloud Sleuth 是分布式系统中跟踪服务间调用的工具,它可以直 ...

  2. redis 简单整理——持久化的问题定位和优化[二十一]

    前言 Redis持久化功能一直是影响Redis性能的高发地,简单介绍一下持久化的问题定位和优化. 正文 当Redis做RDB或AOF重写时,一个必不可少的操作就是执行fork操作创 建子进程,对于大多 ...

  3. This version of Android Studio cannot open this project, please retry with Android Studio 4.0 or newer.

    前言 遇到的问题,This version of Android Studio cannot open this project, please retry with Android Studio 4 ...

  4. 鸿蒙HarmonyOS实战-ArkUI组件(Image)

    一.Image 在HarmonyOS中,Image组件是用于显示图像文件的UI组件.它可以显示本地图像文件或远程URL地址的图像文件.Image组件的实现方式比较简单,只需提供图像文件路径或URL地址 ...

  5. 2024-04-17:用go语言,欢迎各位勇者莅临力扣城,本次的挑战游戏名为「力扣泡泡龙」。 游戏的起点是一颗形状如二叉树的泡泡树,其中每个节点的值代表该泡泡的分值。勇者们有一次机会可以击破一个节点泡

    2024-04-17:用go语言,欢迎各位勇者莅临力扣城,本次的挑战游戏名为「力扣泡泡龙」. 游戏的起点是一颗形状如二叉树的泡泡树,其中每个节点的值代表该泡泡的分值.勇者们有一次机会可以击破一个节点泡 ...

  6. HL7标准的版本

    HL7V2 HL7v2是用于在系统之间交换临床和患者信息的最广泛使用的医疗保健消息传递标准.HL7v2的目标是使用代表临床事件信息的标准化消息(例如患者管理活动.人口统计.医疗订单.结果和财务信息)在 ...

  7. Hologres如何支持超高基数UV计算(基于roaringbitmap实现)

    简介: 本文将会介绍Hologres基于roaringbitmap实现超高基数的UV计算 RoaringBitmap是一种压缩位图索引,RoaringBitmap自身的数据压缩和去重特性十分适合对于大 ...

  8. WPF 设置 IncludePackageReferencesDuringMarkupCompilation 属性导致分析器不工作

    本文记录在 WPF 项目里面设置 IncludePackageReferencesDuringMarkupCompilation 属性为 False 导致了项目所安装的分析器不能符合预期工作 设置 I ...

  9. 2021~2022Apache大数据相关项目盘点

    一.晋升为Apache TLP(Top-Level Project)的大数据相关项目 1.1.Apache DataSketches (20210203) 1.2.Apache Gobblin (20 ...

  10. 【转载】只有.dbf数据文件进行数据库恢复

    此篇文章为转载,来自 " ITPUB博客 " ,链接:http://blog.itpub.net/26015009/viewspace-714742/ 个人mark下,在之后dbf ...