Giter VIP home page Giter VIP logo

blog's Introduction

Hi there 👋 I'm pkjy

  • ⚡ 𝙵𝚞𝚕𝚕-𝚜𝚝𝚊𝚌𝚔 𝚜𝚘𝚏𝚝𝚠𝚊𝚛𝚎 𝚍𝚎𝚟𝚎𝚕𝚘𝚙𝚎𝚛 while using Nodejs & studying Rust.
  • 🔭 Living in Hunan,Changsha, born in Hunan,Huaihua.
  • 👯 Over 5 years FE experience,3 years BF experience, 2 years small group leader experience with Scrum.
  • 🌱 Currently having fun at reverse engineering & Puppeteer RPA.
  • 😄 Stay Hungry, Stay Foolish.

Top Langs

blog's People

Contributors

dependabot[bot] avatar pkjy avatar

Stargazers

 avatar  avatar

Watchers

 avatar

blog's Issues

websocket pcm webaudio 记录

经验

PCM介绍

将音频数字化,其实就是将声音数字化。最常见的方式是透过脉冲编码调制PCM(Pulse Code Modulation) 。运作原理如下。首先我们考虑声音经过麦克风,转换成一连串电压变化的信号。要将这样的信号转为 PCM 格式的方法,是使用三个参数来表示声音,它们是:声道数、采样位数和采样频率。

  • 采样频率:即取样频率,指每秒钟取得声音样本的次数。采样频率越高,声音的质量也就越好,声音的还原也就越真实,但同时它占的资源比较多。由于人耳的分辨率很有限,太高的频率并不能分辨出来。在16位声卡中有22KHz、44KHz等几级,其中,22KHz相当于普通FM广播的音质,44KHz已相当于CD音质了,目前的常用采样频率都不超过48KHz。

  • 采样位数:即采样值或取样值(就是将采样样本幅度量化)。它是用来衡量声音波动变化的一个参数,也可以说是声卡的分辨率。它的数值越大,分辨率也就越高,所发出声音的能力越强。

  • 声道数:很好理解,有单声道和立体声之分,单声道的声音只能使用一个喇叭发声(有的也处理成两个喇叭输出同一个声道的声音),立体声的pcm可以使两个喇叭都发声(一般左右声道有分工) ,更能感受到空间效果。
    image
    image
    image
    图中的黑色曲线表示的是pcm文件录制的自然界的声波,红色曲线表示的是pcm文件输出的声波,横坐标便是采样频率;纵坐标便是采样位数。这几幅图中的格子从左到右,逐渐加密,先是加大横坐标的密度,然后加大纵坐标的密度。显然,当横坐标的单位越小即两个采样时刻的间隔越小,则越有利于保持原始声音的真实情况,换句话说,采样的频率越大则音质越有保证;同理,当纵坐标的单位越小则越有利于音质的提高,即采样的位数越大越好。
    计算机中采样位数一般有8位和16位之分,但有一点请大家注意,8位不是说把纵坐标分成8份,而是分成2的8次方即256份; 同理16位是把纵坐标分成2的16次方65536份;如下图:
    image

存储量=(采样频率X采样位数X声道)X时间/8(单位:字节数)

safari 的特殊情况

  1. safari 播放支持的采样率最低为 22050 Hz 。简直坑爹,相关链接 safari-webkitaudiocontext-createbuffer-api-raises-notsupportederror-exception
  2. safari 录音的输入采样率实测为48000, iphone11, 虽然设置了其他值,但是实际出来的还是48000.
  3. safari 同时只允许4个进行的AudioContext实例,第五个会报错。

更多坑在standardized-audio-context这里面可以看到

AudioContext 限制

Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first

在没有用户交互的情况下,直接进行音频的播放是被浏览器禁止的。可以在有交互的时候就开始音频的播放,然后采取禁音的方法来处理。

g711编码

G711的打包周期分为10ms,20ms,30ms,sample rate是8000,速率是64kbit/s

64kbits,意味着每秒发送64000比特

那么10ms发送= 64000 * (10/1000) = 640 比特 = 80 字节

那么10ms的包 = 80字节
20ms = 160 字节
30ms = 240 字节
那么64kbits 如何来的?
因为采样周期为8000,那么意味着1秒8000个采样,每次采样站8个比特
那么 8000*8 = 64000bit/s = 64kbit/s

接下来计算timestamp的增长量,公式如下:
两帧之间RTP timestamp的增量 = 时钟频率 / 帧率
同样以10ms的G711举例:

10ms的G711帧率 = 100,意味着每秒发送100帧

那么10ms的timestamp增量 = 8000/100 = 80

20ms的timestamp增量 = 160

30ms的timestamp增量 = 240

sdp

m=audio 1234 RTP/AVP 0
a=rtpmap:0 pcma/8000/1
a=framerate:25

c=IN IP4 172.18.168.45

1.m=是媒体级会话的开始处,audio:媒体类型 ; 1234:端口号 ;RTP/AVP:传输协议 ;0:rtp头中的payload格式

2.a=rtpmap:证明是动态绑定的进一步说明 ;0:rtp头中的payload格式;pcma:编码名 ;8000:采样频率;1:单声道

注意:g711有两种编码类型,另一种是pcmu

3.a=framerate:25 指1s播放几个rtp包,单位帧每秒,倒数为一个rtp包承载的数据播放的时间,单位s

8000/25=320 表示每个时间戳增量值 每个rtp包的g711数据大小

4.c=:媒体链接信息;IN:网络类型一般为IN;IP4:地址类型一般为IP4;后面是IP地址(注意是VLC所在的IP地址,不是发送方的IP)

更多sdp介绍参考

websocket 传输二进制数据

websocket可以通过binaryType 属性来选择数据传输的二进制格式,blob会返回Blob类型的原始二进制数据,arraybuffer会返回ArrayBuffer类型的二进制数据。binaryType参考文档

  • 简单来说,Blob是一个类文件(file-like)对象,与File api结合使用。例如文件地址可以通过URL.createObjectURL()来转换成一个本地的url地址,包括audiovideo,他们除了src属性之外,还有srcObject属性,可以直接将Blob地址传给srcObject属性,同样也能正常播放音频与视频。
  • ArrayBuffer是一个提供给js处理二进制数据的数据类型,ArrayBuffer本身是只读的,里面包含了各种类型的TypedArray,是支持直接修改数据的,或者通过DataView视图来修改ArrayBuffer完整的TypedArray参考MDN文档
    如果websocket选择blob类型的话,则需要用File api FileReader.readAsArrayBuffer()把数据读为ArrayBuffer

相关链接

1 ArrayBuffer DataView 数据存储方式 字节序处理

ArrayBuffer DataView 数据存储方式 字节序处理

Javascript在数据的处理上一直不是强项,比如数字不分整型和浮点数统一使用了64位浮点数,如果涉及到二进制运算则显得非常无力,在数据传输上也非常浪费带宽。在ES6针对Javascript二进制数据处理上的无力引入了原始缓冲区ArrayBuffer,并且还提供了多种位数的int类型数组以及数据视图来处理数据。

ArrayBuffer & DataView

图解

用一个 Int8 的确定类型数组来分离存放 8 位二进制字节。
image
用一个无符号的 Int16 数组来分离存放 16 位二进制字节,这样如果是一个无符号的整数也能处理。
image
甚至可以在相同基础的 buffer 上使用不同的 view,同样的操作不同的 view 会给你不同的结果。

比如,如果我们在这个 ArrayBuffer 中从 Int8 view 里获取了元素 0 和 1,在 Uint16 view 中元素 0 会返回给我们不同的值,尽管它们包含的是完全相同的二进制字节。
image
在这种方式中,ArrayBuffer 基本上扮演了一个原生内存的角色,它模拟了像 C 语言才有的那种直接访问内存的方式。

你可能想知道为什么我们不让程序直接访问内存,而是添加了这种抽象层。直接访问内存将导致一些安全漏洞。

更多ArrayBuffer的基础介绍可参考 《JavaScript 标准参考教程》-阮一峰-二进制数组

数据的存储方式

一个整型数字写成2进制之后以书写习惯来说左边是高位,右边是低位。举个例子,int8的5使用二进制是这么表示的:

0000 0101

最左边的0是符号位,表示非负数,101则是5的二进制写法。

负数无法使用非负数的规则来表示,比如-1用非负数的规则表示是:

1000 0001

考虑到-1如果加上1值应该为0,那么该值加上1却会变成-2:

1000 0010

也就是在计算方式上出现问题了,所以二进制是采用了补码的方式来表示,补码可以完全不考虑符号位,它的规则如下:

  • 非负数直接用正常的二进制数表示
  • 负数是绝对值二进制取反加1

按这种规则,-1的绝对值二进制是:

0000 0001

取反后是:

1111 1110

再加1则是:

1111 1111

我们再尝试给它加上1,看看值是多少:

1 0000 0000

明显高位溢出了,于是将高位多余的1截掉变成:

0000 0000

它的值如我们所料是0,这就是补码。uint8由于没有正负之分,它的所有值都是非负数,所以就不需要考虑补码。

数据溢出截断

首先需要了解一下int16用二进制是如何表示的,以3850为例子,它的二进制表示方式是这样子的:

00001111 00001010

int16占了两个字节,因此它被截成两断,我们把左边的字节称为高位字节,右边的字节称为低位字节。在内存中它是如何存放的呢?建立一个Int8Array来看一下:

var buffer = new ArrayBuffer(2);
var int16Array = new Int16Array(buffer);
var int8Array = new Int8Array(buffer);
int16Array[0] = 3850;
// Int16Array [ 3850 ]
console.log(int16Array);
// Int8Array [ 10, 15 ]
console.log(int8Array);

从运行结果看,3850被截断成两个int8值:

int8Array[0] = 10 = 00001010
int8Array[1] = 15 = 00001111

看起来很反人类是吧?高位字节不是放在左边下标为0的字节上,却放在右边下标为1的字节上,跟我们的书写习惯反过来了!

事实上,数据在内存中存储并没有硬性规则高位字节必须放在左边(虽然它更符合人类阅读习惯),具体实现也是根据当前CPU的实现。大多数计算机是以高位优先的顺序存放数据(即高位在左,低位在右),但基于Intel CPU的计算机则是反过来以低位优先存放数据,我的本子就是Intel CPU的,因此我的运行结果就是Int8Array [ 10, 15 ],或许换个电脑就会变成Int8Array [ 15, 10 ]。

那么当数据溢出截断又是怎么处理的?尝试着给一个int8字节赋值3850,看看最终得到的值是什么:

var int8Array = new Int8Array([3850]);
//Int8Array [ 10 ]
console.log(int8Array);

很明显,它把15抛弃了,也就是保存了低位字节,抛弃高位。事实上,溢出处理在各种CPU上都是保留低位能保留的字节,把高位的截断,这个与数据存储是按高位优先还是低位优先没什么关系。

字节序处理

请先看以下示例代码在我机子上跑的情况:

var buffer = new ArrayBuffer(2);
var view = new DataView(buffer);
var int16Array = new Int16Array(buffer);
view.setInt8(0, 10);
// 10
console.log(view.getInt8(0))
// 2560 用二进制表示是:00001010 00000000,
console.log(view.getInt16(0));
int16Array[0] = 10;
// 2560!
console.log(view.getInt16(0));

仔细看结果,很明显,view使用的是高位优先的读写方式,而TypeArray使用的是低位优先的读写方式。在内存中两个字节的数据在setInt8(0, 10)的时候会变成

00001010 00000000

按照我计算机的读写规则应该是低位优先,也就是实际上这里应该被解读成

00000000 00001010

也就是得到10,但实际上使用view却得到了2560,也就是高位优先。

DataView默认是使用big-endian方式,也就是高位优先进行读写。但在读写多字节数据的时候,可以通过传入值为true的little-endian参数来要求使用低位优先规则读写数据。

参考链接

1 《JavaScript 标准参考教程》-阮一峰-二进制数组
2 缓冲数组以及数据视图
3 通俗漫画介绍 ArrayBuffers 和 SharedArrayBuffers

Buffer 二进制 Base64

Buffer 二进制 Base64

Buffer

Buffer是V8引擎上的扩展,这意味着它有其固有的一些限制。Buffer实际上是对内存的直接分配,这意味着这多少受制于你在低级计算机语言方面的经验。JavaScript的其他数据类型都把存储数据的复杂性进行了抽象,而Buffer与它们不同,它提供的是内存的直接操作。创建了一个Buffer后,它的大小就固定了。如果你需要添加更多的数据,就必须把老的Buffer复制到一个更大的Buffer中。虽然有些特性看起来让人沮丧,但它们让Buffer能够在服务器上快速地处理大量的数据操作。这是一个特意的设计选择,为了性能而牺牲了一些程序员的开发便利。 -《Node即学即用》4.3.3 Buffer

简单来说,Buffer是Node提供给我们直接操作二进制数据的类。

二进制

简介

大多数人都知道,计算机的工作原理是操作“开”和“关”状态。因为只有这样两种状态,所以我们称此为二元状态。计算机的所有东西都建立在此基础上,这就说明了为什么在计算机上操作时,直接操作二进制通常是最快的方法。要做更复杂的事情时,我们把比特(bit,每一位表示一个二元状态)集合成8个一组,称之为8位组(octet),也就是通常所说的字节(byte)。1这样我们就能表示除了0、1外的其他数字了。-《Node即学即用》4.3.3 Buffer

8位bit = 1个byte

利用8位字节,我们就可以表示从0到255间的所有数字了。

128 64 32 16 8 4 2 1
0 0 0 0 0 0 0 0 = 0
- - - - - - - -
128 64 32 16 8 4 2 1
1 1 1 1 1 1 1 1 = 255
- - - - - - - -
128 64 32 16 8 4 2 1
1 0 0 1 0 1 0 1 =149

转化的算法就不多加赘述了,高中都有学.. 比如149 = 1x2^7+0x2^6+0x2^5+1x2^4+0x2^3+1x2^2+0x2^1+1x2^0

但是8个0和1组成的字符串来描述实在是不方便,所以**十六进制(HEX)**表示法便流行起来。二进制表示法的基数是2,因此每个数字(0或1)只有两种状态。十六进制使用的基数是16,每一位能够表示0到F种状态,其中字母A到F(或对应的小写字母)对应代表10到15。这样只需要2个字母就能表示整个字节了。最右的位表示1,往左一位就表示16。如果我们要表示数字149,就等价于(16 x 9)+(5 x 1),也就是十六进制的95。

十六进制转换到十进制:

0 1 2 3 4 5 6 7 8 9 A B C D E F
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

十六位进制法计数:

16 1
0 0 = 0
16 1
F F = 255
16 1
9 5 = 149

在JavaScript中,用一个十六进制值表示数字时,需要在其十六进制数值前添加0x标记。比如,0x95表示十进制数字149。在Node里,你会常常看见console.log()输出,或是在Node命令行中,Buffer值是采用十六进制表示的。下面表格就演示了如何用Buffer保存3个字符(比如RGB颜色值)。

> new Buffer([255,0,149]);
<Buffer ff 00 95>
>

Node中创建Buffer默认是UTF-8编码的。

Base64

Base64编码原理

Base64编码是基于64个字符A-Z,a-z,0-9,+,/的编码方式,因为2的6次方正好为64,所以就用6bit就可以表示出64个字符,eg:000000对应A,000001对应B。

**BASE64 的编码原理:**都是按字符串长度,以每 3 个 字符(1Byte=8bit)为一组,然后针对每组,首先获取每个字符的 ASCII 编码(字符'a'=97=01100001),然后将 ASCII 编码转换成 8 bit 的二进制,得到一组 3 * 8=24 bit 的字节。然后再将这 24 bit 划分为 4 个 6 bit 的字节,并在每个 6 bit 的字节前面都填两个高位 0,得到 4 个 8 bit 的字节,然后将这 4 个 8 bit 的字节转换成十进制,对照 BASE64 编码表 (下表),得到对应编码后的字符。
image

注:1. 要求被编码字符是8bit的,所以须在ASCII编码范围内,\u0000-\u00ff,中文就不行。2. 如果被编码字符长度不是3的倍数的时候,则都用0代替,对应的输出字符为“=”

Base64编码本质上是一种将二进制数据转成文本数据的方案。对于非二进制数据,是先将其转换成二进制形式,然后每连续6比特(2的6次方=64)计算其十进制值,根据该值在A--Z,a--z,0--9,+,/ 这64个字符中找到对应的字符,最终得到一个文本字符串。基本规则如下几点:

  1. 标准Base64只有64个字符(英文大小写、数字和+、/)以及用作后缀等号;
  2. Base64是把3个字节变成4个可打印字符,所以Base64编码后的字符串一定能被4整除(不算用作后缀的等号);
  3. 等号一定用作后缀,且数目一定是0个、1个或2个。这是因为如果原文长度不能被3整除,Base64要在后面添加\0凑齐3n位。为了正确还原,添加了几个\0就加上几个等号。显然添加等号的数目只能是0、1或2;
  4. 严格来说Base64不能算是一种加密,只能说是编码转换。

Base64解码原理

解码原理是将4个字节转换成3个字节.先读入4个6位(用或运算),每次左移6位,再右移3次,每次8位,这样就还原了。

Base64编码字符串实例

1、字符长度为能被3整除时:比如“Tom” :

T o m
ASCII: 84 111 109
8bit字节: 01010100 01101111 01101101
6bit字节: 010101 000110 111101 101101
十进制: 21 6 61 45
对应编码: V G 9 t

所以,“Tom”的 BASE64 编码结果为 VG9t。

image

2、字符串长度不能被3整除时,比如“Lucy”:

L u c y
ASCII: 76 117 99 121
8bit字节: 01001100 01110101 01100011 01111001 00000000 00000000
6bit字节: 010011 000111 010101 100011 011110 010000 000000 000000
十进制: 19 7 21 35 30 16 异常 异常
对应编码: T H V j e Q = =

因为4个Base编码为一组,最后再补上'='补齐,即:THVjeQ==

image

ASCII 码使用指定的7 位或8 位二进制数组合来表示128 或256 种可能的字符。标准ASCII 码也叫基础ASCII码,使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符。

ASCII码表可参考《标准表》

Base64编码的应用

  1. 实现简单的数据加密,使用户一眼望去完全看不出真实数据内容,base64算法的复杂程度要小,效率要高相对较高。

  2. Base64编码的主要的作用不在于安全性,而在于让内容能在各个网关间无错的传输,这才是Base64编码的核心作用。

  3. 在计算机中任何数据都是按ascii码存储的,而ascii码的128~255之间的值是不可见字符。而在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个Base64编码,统统变成可见字符,这样出错的可能性就大降低了。

  4. Base64 编码在URL中的应用:

  • Base64编码可用于在HTTP环境下传递较长的标识信息。例如,在Java持久化系统Hibernate中,就采用了Base64来将一个较长的唯一标识符(一般为128-bit的UUID)编码为一个字符串,用作HTTP表单和HTTP GET URL中的参数。在其他应用程序中,也常常需要把二进制数据编码为适合放在URL(包括隐藏表单域)中的形式。此时,采用Base64编码不仅比较简短,同时也具有不可读性,即所编码的数据不会被人用肉眼所直接看到。
  • 然而,标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。

Base64具体实现

  1. 浏览器环境:
  • Base64编码:window.btoa('content')
  • Base64解码:window.atob('content')
  1. Node:

参考资料:

待办

  • Node的Base64编/解码实现

AudioContext,网页录音,音频下载

一张图概括录音+本地播放

前端实现录音有两种方式,一种是使用MediaRecorder,另一种是使用WebRTC的getUserMedia结合AudioContext,MediaRecorder出现得比较早,只不过Safari/Edge等浏览器一直没有实现,所以兼容性不是很好,而WebRTC已经得到了所有主流浏览器的支持,如Safari 11起就支持了。所以我们用WebRTC的方式进行录制。

利用AudioContext播放声音的使用,我已经在《Chrome 66禁止声音自动播放之后》做过介绍,本篇我们会继续用到AudioContext的API.

为实现录音功能,我们先从播放本地文件音乐说起,因为有些API是通用的。

播放本地音频文件实现

播放音频可以使用audio标签,也可以使用AudioContext,audio标签需要一个url,它可以是一个远程的http协议的url,也可以是一个本地的blob协议的url,怎么创建一个本地的url呢?

使用以下html做为说明:
image

提供一个file input上传控件让用户选择本地的文件和一个audio标签准备来播放。当用户选择文件后会触发onchange事件,在onchange回调里面就可以拿到文件的内容,如下代码所示:
image

这里使用一个FileReader读取文件,读取为ArrayBuffer即原始的二进制内容,把它打印如下所示:
image

可以用这个ArrayBuffer实例化一个Uint8Array就能读取它里面的内容,Uint8Array数组里面的每个元素都是一个无符号整型8位数字,即0 ~ 255,相当于每1个字节的0101内容就读取为一个整数。

这个arrayBuffer可以转成一个blob,然后用这个blob生成一个url,如下代码所示:
image

主要利用URL.createObjectURL这个API生成一个blob的url,这个url打印出来是这样的:

blob:null/c2df9f4d-a19d-4016-9fb6-b4899bac630d

然后丢给audio标签就能播放了,作用相当于一个远程的http的url.

在使用ArrayBuffer生成blob对象的时候可以指定文件类型或者叫mime类型,如下代码所示:
image
这个mime可以通过file input的files[0].type得到,而files[0]是一个File实例,File有mime类型,而Blob也有,因为File是继承于Blob的,两者是同根的。所以在上面实现代码里面其实不需要读取为ArrayBuffer然后再封装成一个Blob,直接使用File就行了,如下代码所示:
image

而使用AudioContext需要拿到文件的内容,然后手动进行音频解码才能播放。

AudioContext的模型

使用AudioContext怎么播放声音呢,我们来分析一下它的模型,如下图所示:
image

我们拿到一个ArrayBuffer之后,使用AudioContext的decodeAudioData进行解码,生成一个AudioBuffer实例,把它做为AudioBufferSourceNode对象的buffer属性,这个Node继承于AudioNode,它还有connect和start两个方法,start是开始播放,而在开始播放之前,需要调一下connect,把这个Node连结到audioContext.destination即扬声器设备。代码如下所示:

image

把解码后的audioBuffer打印出来,如下图所示:

image

他有几个对开发人员可见的属性,包括音频时长,声道数量和采样率。从打印的结果可以知道播放的音频是2声道,采样率为44.1k Hz,时长为196.8s。关于声音这些属性的意义可见《从Chrome源码看audio/video流媒体实现一》.

从上面的代码可以看到,利用AudioContext处理声音有一个很重要的枢纽元素AudioNode,上面使用的是AudioBufferSourceNode,它的数据来源于一个解码好的完整的buffer。其它继承于AudioNode的还有GainNode:用于设置音量、BiquadFilterNode:用于滤波、ScriptProcessorNode:提供了一个onaudioprocess的回调让你分析处理音频数据、MediaStreamAudioSourceNode:用于连接麦克风设备,等等。这些结点可以用装饰者模式,一层层connect,如上面代码使用到的bufferSourceNode可以先connect到gainNode,再由gainNode connect到扬声器,就能调整音量了。

如下图示意:
image

这些节点都是使用audioContext的工厂函数创建的,如调createGainNode就可以创建一个gainNode.

说了这么多就是为了录音做准备,录音需要用到ScriptProcessorNode.

录音的实现

上面播放音乐的来源是本地音频文件,而录音的来源是麦克风,为了能够获取调起麦克风并获取数据,需要使用WebRTC的getUserMedia,如下代码所示;
image
在调用getUserMedia的时候指定需要录制音频,如果同时需要录制视频那么再加一个video: true就可以了,也可以指定录制的格式:
image
调用的时候,浏览器会弹一个框,询问用户是否允许使用用麦克风:
image
如果用户点了拒绝,那么会抛异常,在catch里面可以捕获到,而如果一切顺序的话,将会返回一个MediaStream对象:
image
它是音频流的抽象,把这个流用来初始化一个MediaStreamAudioSourceNode对象,然后把这个节点connect连接到一个JavascriptProcessorNode,在它的onaudioprocess里面获取到音频数据,然后保存起来,就得到录音的数据。

如果想直接把录的音直接播放出来的话,那么只要把它connect到扬声器就行了,如下代码所示:
image
但一边录一边播的话,如果没用耳机的话容易产生回音,这里不要播放了。

为了获取录到的音的数据,我们把它connect到一个javascriptProcessorNode,为此先创建一个实例:

image

这里是使用createScriptProcessor创建的对象,需要传三个参数:一个是缓冲区大小,通常设定为4kB,另外两个是输入和输出频道数量,这里设定为双声道。它里面有两个缓冲区,一个是输入inputBuffer,另一个是输出outputBuffer,它们是AudioBuffer实例。可以在onaudioprocess回调里面获取到inputBuffer的数据,处理之后,然后放到outputBuffer,如下图所示:
image
例如我们可以把第1步播放本音频用到的bufferSourceNode连接到jsNode,然后jsNode再连接到扬声器,就能在process回调里面分批处理声音的数据,如降噪。当扬声器把4kB的outputBuffer消费完之后,就会触发process回调。所以process回调是不断触发的。

在录音的例子里,是要把mediaNode连接到这个jsNode,进而拿到录音的数据,把这些数据不断地push到一个数组,直到录音终止了。如下代码所示:

image

我们把inputBuffer打印出来,可以看到每一段大概是0.09s:

image
也就是说每隔0.09秒就会触发一次。接下来的工作就是在process回调里面把录音的数据持续地保存起来,如下代码所示,分别获取到左声道和右声道的数据:
image
打印出来可以看到它是一个Float32Array,即数组里的每个数字都是32位的单精度浮点数,如下图所示:

这里有个问题,录音的数据到底表示的是什么呢,它是采样采来的表示声音的强弱,声波被麦克风转换为不同强度的电流信号,这些数字就代表了信号的强弱。它的取值范围是[-1, 1],表示一个相对比例。

然后不断地push到一个array里面:

image
最后加一个停止录音的按钮,并响应操作:
image
把保存的数据打印出来是这样的:

是一个普通数组里面有很多个Float32Array,接下来它们合成一个单个Float32Array:
image

那为什么一开始不直接就弄成一个单个的,因为这种Array不太方便扩容。一开始不知道数组总的长度,因为不确定要录多长,所以等结束录音的时候再合并一下比较方便。

然后把左右声道的数据合并一下,wav格式存储的时候并不是先放左声道再放右声道的,而是一个左声道数据,一个右声道数据交叉放的,如下代码所示:
image

最后创建一个wav文件,首先写入wav的头部信息,包括设置声道、采样率、位声等,如下代码所示:
image

接下来写入录音数据,我们准备写入16位位深即用16位二进制表示声音的强弱,16位表示的范围是 [-32768, +32767],最大值是32767即0x7FFF,录音数据的取值范围是[-1, 1],表示相对比例,用这个比例乘以最大值就是实际要存储的值。如下代码所示:
image
最后,再用第1点提到的生成一个本地播放的blob url就能够播放刚刚录的音了,如下代码所示:
image
或者是把blob使用FormData进行上传。

整一个录音的实现基本就结束了,代码参考了一个录音库RecordRTC。

小结

回顾一下,整体的流程是这样的:
image
先调用webRTC的getUserMediaStream获取音频流,用这个流初始化一个mediaNode,把它connect连接到一个jsNode,在jsNode的process回调里面不断地获取到录音的数据,停止录音后,把这些数据合并换算成16位的整型数据,并写入wav头部信息生成一个wav音频文件的内存buffer,把这个buffer封装成Blob文件,生成一个url,就能够在本地播放,或者是借助FormData进行上传。这个过程理解了就不是很复杂了。

本篇涉及到了WebRTC和AudioContext的API,重点介绍了AudioContext整体的模型,并且知道了音频数据实际上就是声音强弱的记录,存储的时候通过乘以16位整数的最大值换算成16位位深的表示。同时可利用blob和URL.createObjectURL生成一个本地数据的blob链接。

RecordRTC录音库最后面还使用了webworker进行合并左右声道数据和生成wav文件,可进一步提高效率,避免录音文件太大后面处理的时候卡住了。

参考资料:

待办

音频下载

关于个人博客

一直觉得写博客是一件非常需要精力的事,从大学那会自学开始,就有尝试过在githubfront-end-skills、csdnpkjy上记录过一些Demo的编程过程和一些翻译,读书笔记。很多时候一篇简单的文章下来,花费的时间绝对是要比单做一些Demo要高出几倍。

其次也先后考虑过文章入数据库或者完全静态Markdown到本地。当然本地输出Markdown也是我入门Node的首次尝试,虽然没有完全利用起来(简单了写了一个Markdown内容转为所需JSON文件的小脚本)。

在几年的学习中,陆陆续续发现了很多优质的文章,也都收藏了起来,有网页收藏夹形式的,也有各网站自带的收藏功能,不过这些内容一来是比较分散,不方便归类;再者不知道是什么缘由,有部分的文章会过期或者被删除掉了。(pocket有在用,但是文章链接本身是不可靠的,所以未能解决上面提到的一些问题)

因此为了更好的更永久的保存下来经过自己确认过的好文章,并且统一一个入口来进行管理,在此决定用issue记录博客的内容,也有很多把博客放在issue里的大佬,这里还看到一份收录博客相关相关工具炒鸡多的issue博客系统、文档生成。有体验过语雀,个人博客,最终选择。使用起来确实很舒服,不过毕竟是商业的东西,心里总觉得不踏实。目前有图床并且能写md就已经很满足我的要求了,剩下的就是把以前零零散散看到的深度好文都给换到这个新家来喽,当然这也意味着放弃了流量,毕竟csdn,掘金等技术网站在流量上还是很有优势的。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.