|
| 1 | +--- |
| 2 | + title: 浏览器调用麦克风权限实现语音输入功能 |
| 3 | + date: 2023-10-19T10:12:35Z |
| 4 | + summary: |
| 5 | + tags: ["WebAPI"] |
| 6 | +--- |
| 7 | + |
| 8 | + ### 前言 |
| 9 | +今天收到一个需求:在一个聊天室场景中,除了用户除了打字输入,还要求能语音输入,实现这个需求我使用到了浏览器的相关的API、阿里云的语音转换API。 |
| 10 | + |
| 11 | + |
| 12 | +### 采集音频 |
| 13 | +首先我们得采集到用户说的话,实现这个得使用到两个浏览器的API,第一个是<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia" target="_blank">MediaDevices.getUserMedia()</a> `MediaDevices.getUserMedia()`会提示用户给予使用媒体输入的许可,媒体输入会产生一个[MediaStream](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStream),里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D 转换器等等),也可能是其他轨道类型。 |
| 14 | + |
| 15 | +通俗的来讲,就是可以拉取获取用户媒体权限的弹框,在用户通用后获取一个流。值得注意的是:在实现的过程中这个弹框的触发在各个浏览器中是不同的,在苹果的safari浏览器中获取权限框被用户拒绝后依旧能再次触发,但在某些安卓浏览器中权限框被拒绝后就会维持拒绝状态不会被触发;第二,获取权限和用户语音输入的时机最好错开,作者要实现的交互是按住进行语音输入,松开发送,但某些安卓浏览器在触发获取权限弹窗时会将`touchend`事件的图层覆盖掉,使之不能触发。需要注意的是这个API想要使用有诸多限制,首先只能在https协议下使用,其次使用的设备要有相关的媒体硬件,最后就是需要授权了。 |
| 16 | +``` |
| 17 | + navigator.mediaDevices |
| 18 | + .getUserMedia({ |
| 19 | + audio: true, |
| 20 | + }) |
| 21 | + .then((res) => { |
| 22 | + speechState.stream = res; |
| 23 | + }) |
| 24 | + .catch(() => { |
| 25 | + Toast("抱歉,我们无法获取您的语音权限,无法使用语音输入功能"); |
| 26 | + }) |
| 27 | +``` |
| 28 | + |
| 29 | +第二步,我们可以通过[MediaRecorder](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder)进行录制: |
| 30 | +``` |
| 31 | +//创建实例 |
| 32 | +speechState.recordInstance = new MediaRecorder(speechState.stream); |
| 33 | +// 录制事件 |
| 34 | + speechState.recordInstance.ondataavailable = (event) => { |
| 35 | + if (event.data.size > 0) { |
| 36 | + // 将每次返回的二进制数据存入数组 |
| 37 | + chunks.push(event.data); |
| 38 | + } |
| 39 | + }; |
| 40 | +// 开始事件 |
| 41 | +speechState.recordInstance.start(); |
| 42 | +
|
| 43 | +// 停止事件 |
| 44 | +// 将获取到的二进制数据进行合并,然后转换为文字 |
| 45 | +speechState.recordInstance.onstop = () => { |
| 46 | + const audioBlob = new Blob(chunks, { type: "audio/wav" }); |
| 47 | + const formData = new FormData(); |
| 48 | + formData.append("file", audioBlob); |
| 49 | + voice2Text(formData); |
| 50 | +}; |
| 51 | +``` |
| 52 | + |
| 53 | + |
| 54 | +### 转化音频 |
| 55 | +这一步我们选用了阿里云的语音转化接口,但作者在写的时候没有弄清楚阿里云接口所接收的格式,所以卡了很久: |
| 56 | +``` |
| 57 | +// 上传语音 |
| 58 | +const voice2Text = (payload) => { |
| 59 | + Toast.loading({ |
| 60 | + message: "加载中...", |
| 61 | + forbidClick: true, |
| 62 | + }); |
| 63 | +
|
| 64 | + arsVoiceSynthesis(payload).then((res) => { |
| 65 | + if (res.data.code == 500) { |
| 66 | + Toast.fail("语音处理失败,请重试"); |
| 67 | + } else { |
| 68 | + speechState.result = res.result; |
| 69 | + chatState.messageList.push({ |
| 70 | + message: res.data.message, |
| 71 | + isSelf: true, |
| 72 | + }); |
| 73 | + scrollToBottom(); |
| 74 | + sendMessage(res.data.message); |
| 75 | + } |
| 76 | +
|
| 77 | + Toast.clear(); |
| 78 | + }); |
| 79 | +}; |
| 80 | +``` |
| 81 | +值得注意的是,阿里云的接口接收的数据类型有不同的要求,分别是`pcm`和`wav`格式,如何不注意类型可能会解析不出来,他们的区别我整理如下: |
| 82 | + |
| 83 | +* PCM |
| 84 | +PCM(Pulse Code Modulation----脉码调制录音)。所谓PCM录音就是将声音等模拟信号变成符号化的脉冲列,再予以记录。PCM信号是由[1]、[0]等符号构成的数字信号,而未经过任何编码和压缩处理。与模拟信号比,它不易受传送系统的杂波及失真的影响。动态范围宽,可得到音质相当好的影响效果。 |
| 85 | + |
| 86 | +* WAV |
| 87 | +wav是一种无损的音频文件格式,WAV符合 PIFF(Resource Interchange File Format)规范。所有的WAV都有一个文件头,这个文件头音频流的编码参数。WAV对音频流的编码没有硬性规定,除了PCM之外,还有几乎所有支持ACM规范的编码都可以为WAV的音频流进行编码。 |
| 88 | + |
| 89 | +简单的来说,PCM 就是比 WAV 少一个文件头,因此你可以将他们互相转化。 |
| 90 | + |
| 91 | +### 发送音频 |
| 92 | +其实到这里我们的功能差不多已经实现,接下来就是通过`WebSocket`发送,我就不多赘述了。 |
| 93 | + |
| 94 | + |
| 95 | +### 总结 |
| 96 | +这个需求完成的过程中的三个重点分别是:不同浏览器获取权限的适配、语音流的获取处理、不同语音格式的区分和转化。 |
| 97 | + |
| 98 | + |
| 99 | + |
| 100 | + |
| 101 | + |
0 commit comments