前端AI语音方面的实现

2,061 阅读16分钟

感谢内容提供者:金牛区吴迪软件开发工作室

前言

今天我们来点有意思的,AI语音转换!当我们遇到语音转换的需求感觉,哇,好难啊,这怎么开发啊。其实这是很简单的,今天笔者就来给大家演示一下我们用js实现语音转换功能!

首先我们先来做俩个按钮,一个开始按钮一个结束并开始转换语音的按钮: 在这里插入图片描述

第一章:语音识别

一、开始写代码

写html和简单的css

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        * {margin: 0;padding: 0;}
        body {
            display: flex;
            justify-content: space-around;
            align-items: center;
        }
        .startBtn, .endBtn {
            display: block;
            width: 150px;
            height: 120px;
            border: 2px solid;
            margin: 10px;
        }
    </style>
</head>
<body>
    <button onclick="start()" class="startBtn">开始说话</button>
    <br />
    <button onclick="end()" class="endBtn">结束并转换普通话</button>
</body>
</html>

然后我们在控制台搞一些调试信息,并将最后的转换结果alert出来!

js代码

<script>
    var recognition = new webkitSpeechRecognition() || new SpeechRecognition();
    recognition.lang = 'cmn-Hans-CN'; //定义普通话 (中国大陆)
    function start() {
        console.log('start')
        // 开启
        recognition.start();
    }
    function end() {
        console.log('end')
        // 停止
        recognition.stop();
    }
    // 当调用recognition的stop的时候会触发此对象的onresult事件,然后我们在这里获取我们的转换结果。
    recognition.onresult = function(event) {
        alert(event.results[0][0].transcript);
    }
</script>

二、知识点讲解

我们可以把这个SpeechRecognition内置对象console出来看看他都有什么属性和方法 在这里插入图片描述

属性介绍:

属性作用参数
grammars此属性存储了SpeechGrammar对象的集合,这些对象表示对此识别有效的语法。参考地址
lang此属性将使用有效的BCP 47语言标签设置请求识别的语言。如果没有设置它仍然未设置为脚本获得,但将默认使用该语言的HTML文档的根元素和相关层次的。当输入请求打开与识别服务的连接时,将计算并使用此默认值。与html的lang属性一样,都是枚举
continuous此属性设置为false时,用户代理必须响应于开始识别(例如,交互的单个转弯模式)返回不超过一个最终结果。当其设置为true时,用户代理必须响应于开始的识别(例如听写),返回代表多个连续识别的零个或多个最终结果。默认值必须为false。此属性的设置不会影响中期结果。boolean
interimResults控制是否返回中期结果,设置为true时,应返回中期结果。设置为false时,不得返回中期结果。默认值必须为false。此属性不会影响最终结果。boolean
maxAlternatives此属性设置SpeechRecognitionAlternative每个结果的最大数量。默认值1。其值在[0,18446744073709551615]范围内

方法介绍:

方法介绍
start()调用start方法时,它表示Web应用程序希望开始识别的时间。当语音输入通过输入媒体流实时流式传输时,此开始呼叫表示该服务必须开始收听并尝试匹配与此请求关联的语法的时间。一旦系统成功侦听识别,用户代理必须引发一个开始事件。如果在已经启动的对象上调用了start方法(也就是说,先前已经调用过start,并且没有在该对象上引发错误或结束事件),则用户代理必须抛出“ InvalidStateError” DOMException并忽略该调用。
stop()stop方法表示对识别服务的一条指令,该指令停止听更多的音频,并尝试仅使用已为该识别接收的音频来返回结果。stop方法的典型用法可能是用于Web应用程序,其中最终用户正在进行端点指向,类似于对讲机。最终用户可能会按住空格键与系统对话,而在按下空格键时会发生开始调用,并且在释放空格键时会调用stop方法,以确保系统不再监听用户。调用stop方法后,语音服务不得收集其他音频,也不得继续收听用户的声音。语音服务必须尝试根据已为该识别而收集的音频返回识别结果(或不匹配)。如果在已经停止或正在停止的对象上调用stop方法(即从未在其上调用start),发生了end或error事件,或者先前已调用stop),则用户代理必须忽略该调用。
abort()中止方法是一种请求,要求立即停止侦听并停止识别,并且不返回任何信息,但系统已完成。调用中止方法时,语音服务必须停止识别。一旦语音服务不再连接,用户代理必须引发结束事件。如果在已经停止或正在中止的对象上调用了中止方法(也就是说,从未在其上调用start,在其上引发了end或error事件,或者先前已在其上调用过中止),则用户代理必须忽略电话。

事件介绍

事件介绍
audiostart用户代理开始捕获音频时触发
soundstart当检测到某种声音(可能是语音)时触发。这必须以低延迟触发,例如通过使用客户端能量检测器。该audiostart事件必须已在soundstart事件之前发射。
speechstart当将用于语音识别的语音开始时触发。该audiostart事件必须已在speechstart事件之前发射。
speechend当将用于语音识别的语音结束时触发。该speechstart事件必须始终被speechend之前解雇
soundend当不再检测到某些声音时触发。这必须以低延迟触发,例如通过使用客户端能量检测器。该soundstart事件必须始终被soundend之前解雇。
audioend用户代理完成音频捕获后触发。该audiostart事件必须始终被audioend之前解雇。
result当语音识别器返回结果时触发。该事件必须使用SpeechRecognitionEvent接口。该audiostart事件必须已在结果事件之前发射。
nomatch当语音识别器返回没有识别假设达到或超过置信度阈值的最终结果时触发。该事件必须使用SpeechRecognitionEvent接口。results事件中的属性可能包含低于置信度阈值或为空的语音识别结果。该audiostart事件必须已在NOMATCH事件之前发射。
error发生语音识别错误时触发。该事件必须使用SpeechRecognitionErrorEvent接口
start当识别服务开始收听音频以进行识别时触发
end服务断开连接时触发。无论会话结束的原因如何,都必须始终在会话结束时生成事件。

三、兼容性介绍

非常遗憾的告诉大家这个api的兼容性并不好,一般笔者只会在chrome上面使用它,如果不是chrome浏览器的话那么笔者就会调用后端的接口。比如用java或者python等语言实现。 在这里插入图片描述

很多朋友很奇怪,既然兼容性不好为什么要用它,因为调用后端接口势必就会给服务器带来一定的压力,而且还和网速等等有很大的关系。我们前端自己能实现的话肯定是优先我们自身实现。

兼容写法

<script>
    function start() {
        voiceConversion('start')
    }
    function end() {
        voiceConversion('end')
    }
    function voiceConversion(type) {
        console.log(type);
        try {
            let recognition = new webkitSpeechRecognition() || new SpeechRecognition();
            console.log(recognition)
            recognition.lang = 'cmn-Hans-CN'; //普通话 (中国大陆)
            if (type === 'start') {
                // 开启
                recognition.start();
            } else {
                // 停止
                recognition.stop();
            }
            recognition.onresult = function(event) {
                alert(event.results[0][0].transcript);
            }
        } catch (e) {
            console.log('调用后端接口');
        }
    }
</script>

四、实战演习

我们接下来给大家展示一下当用户点击录音的时候实时的识别出用户说的话并展示出来:

全部代码:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        * {margin: 0;padding: 0;}
        .wrapBox {
            display: flex;
            justify-content: space-around;
            align-items: center;
        }
        .startBtn, .endBtn {
            display: block;
            width: 150px;
            height: 120px;
            border: 2px solid;
            margin: 10px;
        }
    </style>
</head>
<body>
    <div class="wrapBox">
        <button onclick="start()" class="startBtn">开始说话</button>
        <br />
        <button onclick="end()" class="endBtn">结束并转换普通话</button>
    </div>
    <div id="resultText"></div>
    <script>
        const resultTextDiv = document.querySelector('#resultText');
        function start() {
            voiceConversion('start')
        }
        function end() {
            voiceConversion('end')
        }
        function voiceConversion(type) {
            console.log(type);
            try {
                let recognition = new webkitSpeechRecognition() || new SpeechRecognition();
                recognition.lang = 'cmn-Hans-CN'; //普通话 (中国大陆)
                recognition.interimResults = true;
                if (type === 'start') {
                    // 开启
                    recognition.start();
                } else {
                    // 停止
                    recognition.stop();
                }
                recognition.onresult = function(event) {
                    const text = event.results[0][0].transcript;
                    resultTextDiv.innerHTML = text;
                }
            } catch (e) {
                console.log('调用后端接口');
            }
        }
    </script>
</body>
</html>

结尾:

这只是我们开始的一个最基本的需求,我们还需要加入获取声音大小的需求以及复读的功能

第二章:判断语音声音大小做个小样式

一:根据第一个需求代码进行拓展

我们需要在里面追加一个html标签,用来展示我们的声音大小:

<span id="volumeBox"></span>

然后定义一个检测声音大小的函数:

// 获取我们的音量盒子元素
const mystatus = document.getElementById('volumeBox');
let mediaStreamSource = null,
    scriptProcessor = null;
// 开始检测音量大小的函数
function beginDetect() {
    let audioContext = new (window.AudioContext || window.webkitAudioContext)();

    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        // 获取用户的 media 信息
        navigator.mediaDevices.getUserMedia({audio: true}).then((stream) => {
            // 将麦克风的声音输入这个对象
            mediaStreamSource = audioContext.createMediaStreamSource(stream);
            // 创建一个音频分析对象,采样的缓冲区大小为4096,输入和输出都是单声道
            scriptProcessor = audioContext.createScriptProcessor(4096,1,1);
            // 将该分析对象与麦克风音频进行连接
            mediaStreamSource.connect(scriptProcessor);
            // 解决 Chrome 自身的 bug
            scriptProcessor.connect(audioContext.destination);

            // 开始处理音频
            scriptProcessor.onaudioprocess = (e) => {
                // 获得缓冲区的输入音频,转换为包含了PCM通道数据的32位浮点数组
                const buffer = e.inputBuffer.getChannelData(0);
                // 获取缓冲区中最大的音量值
                const maxVal = Math.max.apply(Math, buffer);
                // 显示音量值
                mystatus.innerHTML = `您的音量值:${ Math.round(maxVal * 100)}`;
            };
        }).catch((error) => {
            mystatus.innerHTML = `获取音频时好像出了点问题。${error}`;
        })
    } else {
        mystatus.innerHTML = '您的浏览器不支持识别声音大小';
    }
};
// 关闭检测音量大小
function stopDetect() {
    scriptProcessor.disconnect 
    	? scriptProcessor.disconnect() 
    	: console.log('无需关闭任何');
}

然后我们分别在开始说话和结束说话的时候调用这俩个函数:

// 在voiceConversion函数里进行局部追加代码
if (type === 'start') {
    // 开启
    beginDetect();
    recognition.start();
} else {
    // 停止
    stopDetect();
    recognition.stop();
}

我们可以根据maxValue进行设计一些其他的样式。而不是像笔者发布在这里仅仅展示了字体而已。 比如【类似这种】:在这里插入图片描述

有想要这种类似的样式的可以私聊笔者,由于本博文内容过多,代码就不放出来了哈~。

二、知识点整理

mediaDevices

方法介绍
MediaDevices.enumerateDevices()获取有关系统中可用的媒体输入和输出的一系列信息
getSupportedConstraints()返回一个对象,该对象符合MediaTrackSupportedConstraints指示MediaStreamTrack接口上支持哪些可约束属性的信息。
getDisplayMedia()提示用户选择一个显示器或显示器的一部分(例如窗口)以捕获MediaStream为共享或记录目的。返回解析为的Promise MediaStream。
MediaDevices.getUserMedia()在用户通过提示允许的情况下,打开系统上的相机或屏幕共享和/或麦克风,并提供 MediaStream 包含视频轨道和/或音频轨道的输入。

AudioContext

该AudioContext接口表示由链接在一起的音频模块构建的音频处理图,每个音频模块由表示AudioNode。音频上下文既控制其包含的节点的创建,也控制音频处理或解码的执行。您需要先创建一个,AudioContext然后再执行其他操作,因为所有事情都在上下文中发生。

方法介绍
AudioContext.close()关闭音频上下文,释放它使用的所有系统音频资源。
AudioContext.createMediaElementSource()创建与MediaElementAudioSourceNode相关联HTMLMediaElement。这可用于播放和操作来自或元素的音频。
AudioContext.createMediaStreamSource()创建一个MediaStreamAudioSourceNode与MediaStream表示相关联的音频流,该音频流可能来自本地计算机麦克风或其他来源。
AudioContext.createMediaStreamDestination()创建MediaStreamAudioDestinationNode与MediaStream表示一个音频流的关联,该音频流可以存储在本地文件中或发送到另一台计算机。
AudioContext.createMediaStreamTrackSource()创建MediaStreamTrackAudioSourceNode与一个MediaStream表示媒体流轨道的关联。
AudioContext.getOutputTimestamp()返回一个新AudioTimestamp对象,其中包含两个与当前音频上下文有关的音频时间戳值
AudioContext.resume()在以前已暂停/暂停的音频上下文中恢复时间的进展。
AudioContext.suspend()挂起音频环境中的时间,暂时停止音频硬件访问并减少进程中的CPU /电池使用量。
AudioContext.createScriptProcessor()AudioContext 接口的createScriptProcessor() 方法创建一个ScriptProcessorNode 用于通过JavaScript直接处理音频.

createScriptProcessor()接收三个参数: ==bufferSize== 缓冲区大小,以样本帧为单位。具体来讲,缓冲区大小必须是下面这些值当中的某一个: 256, 512, 1024, 2048, 4096, 8192, 16384. 如果不传,或者参数为0,则取当前环境最合适的缓冲区大小, 取值为2的幂次方的一个常数,在该node的整个生命周期中都不变. 该取值控制着audioprocess事件被分派的频率,以及每一次调用多少样本帧被处理. 较低bufferSzie将导致一定的延迟。较高的bufferSzie就要注意避免音频的崩溃和故障。推荐作者不要给定具体的缓冲区大小,让系统自己选一个好的值来平衡延迟和音频质量。 ==numberOfInputChannels== 值为整数,用于指定输入node的声道的数量,默认值是2,最高能取32. ==numberOfOutputChannels== 值为整数,用于指定输出node的声道的数量,默认值是2,最高能取32.

语法: var audioCtx = new AudioContext(); myScriptProcessor = audioCtx.createScriptProcessor(bufferSize, numberOfInputChannels, numberOfOutputChannels);

三、兼容性介绍

MediaDevices除了IE全军覆没以外,其他的浏览器支持情况良好 AudioContext也是一样的。

在这里插入图片描述

第三章:浏览器复读功能实现

一、代码展示

<!--追加一个选择语言的select标签-->
<div>选择语言:<select name="" id=""></select></div>

首先获取浏览器支持的语言,然后追加option标签放到select标签里供我们选择 注意点:最好将recognition.interimResults = true;去掉,否者交互是非常不友善的。

// 首先获取浏览器支持的语言
const synth = window.speechSynthesis;
let voices = synth.getVoices();
const voiceSelect = document.querySelector('select');
function populateVoiceList() {
    voices = synth.getVoices();
    for(let i = 0; i < voices.length ; i++) {
        const option = document.createElement('option');
        option.textContent = voices[i].name + ' (' + voices[i].lang + ')';
        if(voices[i].default) {
            option.textContent += ' -- DEFAULT';
        }
        option.setAttribute('data-lang', voices[i].lang);
        option.setAttribute('data-name', voices[i].name);
        voiceSelect.appendChild(option);
    }
}
populateVoiceList();
if (synth.onvoiceschanged !== undefined) {
   synth.onvoiceschanged = populateVoiceList;
}

在我们recognition.onresult那里把获取到的text朗诵出来

const utterThis = new window.SpeechSynthesisUtterance(text);
const selectedOption = voiceSelect.selectedOptions[0].getAttribute('data-name');
for(let i = 0; i < voices.length;i ++){
    if(voices[i].name === selectedOption){
        utterThis.voice =voices[i];
    }
}
synth.speak(utterThis);

二、知识点整理

首先是SpeechSynthesisUtterance对象,主要用来构建语音合成实例,例如上面代码中的实例对象utterThis。我们可以直接在构建的时候就把要读的文字内容写进去。也可以在后面设置它的text属性。

属性介绍
text此属性指定为此语音要合成和说出的文本。这可以是纯文本,也可以是格式正确的完整SSML文档。[SSML]对于不支持SSML或仅支持某些标签的语音合成引擎,用户代理或语音引擎必须剥离不支持的标签并说出文字。文本的最大长度可能限制为32,767个字符。
lang此属性使用有效的BCP 47语言标签为语音指定语音合成的语言。[BCP47]如果没有设置它仍然未设置为脚本获得,但将默认使用该语言的HTML文档的根元素和相关层次的。当输入请求打开与识别服务的连接时,将计算并使用此默认值。
voice类型为SpeechSynthesisVoice,可为空此属性指定Web应用程序希望使用的语音合成语音。当一个SpeechSynthesisUtterance对象被创建这个属性必须被初始化为null。如果在speak()方法调用时将此属性设置为所SpeechSynthesisVoice返回的对象之一getVoices(),则用户代理必须使用该声音。如果在speak()方法调用时此属性未设置或为null ,则用户代理必须使用用户代理的默认语音。用户代理默认语音应支持当前语言(请参阅参考资料lang),并且可以是本地或远程语音服务,并且可以通过用户代理提供的接口(例如浏览器配置参数)合并最终用户的选择。
volume此属性指定话语的说话音量。它的范围是0到1(含0和1),其中0是最低音量,1是最高音量,默认值为1。如果使用SSML,则此值将被标记中的韵律标记覆盖。
rate此属性指定话语的语速。这是相对于此语音的默认速率。1是语音合成引擎或特定语音支持的默认速率(应对应于正常的语音速率)。2是两倍的速度,而0.5是一半的速度。严格禁止小于0.1或大于10的值,但是语音合成引擎或特定语音可能会进一步限制最小和最大速率,例如,即使您指定的值大于3,特定语音实际上可能不会比正常语音快3倍。如果使用SSML,则此值将被标记中的prosody标签覆盖。
pitch此属性指定发声的说话音调。范围在0到2之间(含0和2),其中0是最低音高,2是最高音高。1对应于语音合成引擎或特定语音的默认音高。语音合成引擎或语音可能会进一步限制最小和最大速率。如果使用SSML,则该值将被标记中的prosody标签覆盖。

然后是SpeechSynthesis对象。它是用于控制文本到语音输出的脚本化Web API。

属性介绍
pending如果全局SpeechSynthesis实例的队列包含尚未开始讲话的所有话语,则此属性为true。
speaking如果正在说语音,则此属性为true。具体来说,如果已经开始说出话语而还没有完成说出话语。这与全局SpeechSynthesis实例是否处于暂停状态无关。
paused当全局SpeechSynthesis实例处于暂停状态时,此属性为true。此状态与队列中是否有任何内容无关。新窗口的全局SpeechSynthesis实例的默认状态为非暂停状态。
方法介绍
speak(utterance)对于全局SpeechSynthesis实例,此方法将SpeechSynthesisUtterance对象的发音附加到队列的末尾。它不会更改SpeechSynthesis实例的暂停状态。如果SpeechSynthesis实例已暂停,它将保持暂停状态。如果未暂停并且队列中没有其他语音,则立即说出该语音,否则将该语音排入队列,以便在队列中的其他语音都说完之后开始讲话。如果在调用此方法之后且在相应结束或错误之前对SpeechSynthesisUtterance对象进行了更改事件,但未定义这些更改是否会影响您所说的内容,并且这些更改可能会导致返回错误。SpeechSynthesis对象获得了SpeechSynthesisUtterance对象的专有所有权。将其作为speak()参数传递给另一个SpeechSynthesis对象应引发异常。(例如,两个框架可能具有相同的原点,并且每个框架都将包含一个SpeechSynthesis对象。)
cancel()此方法从队列中删除所有语音。如果说出话语,则说话立即停止。此方法不会更改全局SpeechSynthesis实例的暂停状态。
pause()此方法将全局SpeechSynthesis实例置于暂停状态。如果正在说出话语,它将暂停中间的话语。(如果在SpeechSynthesis实例已经处于暂停状态时调用,它将不执行任何操作。)
resume()此方法将全局SpeechSynthesis实例置于非暂停状态。如果正在说出话语,它将在暂停时继续说出话语,否则它将开始说出队列中的下一个话语(如果有)。(如果在SpeechSynthesis实例已经处于非暂停状态时调用,它将不执行任何操作。)
getVoices()此方法返回可用的声音。用户语音取决于哪些声音可用。如果没有可用的语音,或者尚不清楚可用语音列表(例如:服务器端综合,其中异步确定列表),则此方法必须返回长度为零的SpeechSynthesisVoiceList。
voiceschanged当getVoices方法将返回的SpeechSynthesisVoiceList的内容已更改时触发。示例包括:服务器端综合,其中异步确定列表,或者安装/卸载客户端语音。

三、兼容性介绍

在这里插入图片描述 在这里插入图片描述

第四章:最终效果展示:

在这里插入图片描述