Skip to content

实时语音通信

参考

代码中涉及到的变量
  • ws: <WebSocket | null>null websocket

  • isConnected: false websocket 连接状态

  • buffer: <any[]>[] 录音缓存

  • size: 0 录音文件长度

  • context: <AudioContext | null>null 录音文件长度

  • audioInput: <MediaStreamAudioSourceNode | null>null 媒体流音频源

  • recorder: <ScriptProcessorNode | null>null 录音节点

  • oututSampleBits: 16 输出采样数位

  • receiveData: <Blob[]>[] 输出音频数据集

开启麦克风

js
//首先判断浏览器是否支持WebRTC
const init = () => {
	if (navigator.mediaDevices.getUserMedia) {
		const constraints = { audio: true }; //此处我们仅开启音频
		//判断是否有输入设备(麦克风)
		navigator.mediaDevices.getUserMedia(constraints).then(
			(stream) => {
				context = new AudioContext();
				audioInput = context.createMediaStreamSource(stream);
				recorder = context.createScriptProcessor(4096, 1, 1);

				//这里监听麦克风实时的数据
				recorder.onaudioprocess = (e) => {
					const inputBuffer = e.inputBuffer.getChannelData(0);
					const data = new Float32Array(inputBuffer);
					buffer.push(data);
					size += data.length;

					sendData(); //将数据发送至服务端

					//数据每次发送完将旧数据清空
					buffer = [];
					size = 0;
				};

				audioInput.connect(recorder);
				recorder.connect(context.destination);
			},
			(err) => {
				console.error('未发现设备');
			}
		);
	} else {
		console.error('浏览器不支持');
	}
};

将麦克风数据进行编码和压缩

js
//转码
const encodePCM = () => {
	const bytes = combine();
	const sampleBits = oututSampleBits; //16
	let offset = 0;
	const dataLength = bytes.length * (sampleBits / 8); //采样位如果是16位,一个自己字节8位,一个采样点要2个字节,所以长度要乘以2,所以dataLength就是字节数
	const buffer = new ArrayBuffer(dataLength);
	const data = new DataView(buffer);

	if (dataLength > 65535) {
		console.error('数据过长:');
		return;
	}
	for (let i = 0; i < bytes.length; i++, offset += 2) {
		const s = Math.max(-1, Math.min(1, bytes[i]));
		data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
	}
	return data;
};
// 数据简单处理48K转8k,每隔6个采样点取一个,相当于压缩了6倍
const combine = () => {
	// 合并
	const data = new Float32Array(size);
	let offset = 0; // 偏移量计算
	// 将二维数据,转成一维数据
	for (let i = 0; i < buffer.length; i++) {
		data.set(buffer[i], offset);
		offset += buffer[i].length;
	}
	//return data;

	//按比例压缩 48K --> 8K 6倍
	const compression = parseInt('6');
	const length = data.length / compression;
	const result = new Float32Array(length);
	let index = 0,
		j = 0;
	while (index < length) {
		result[index] = data[j];
		j += compression;
		index++;
	}
	return result;
};

初始化 websocket

js
/**
 * @param wsUrl 连接地址
 */
const initWs = (wsUrl) => {
	try {
		ws = new WebSocket(wsUrl);

		ws.onopen = () => {
			isConnected = true;
		};
		ws.onclose = () => {
			if (recorder) {
				recorder.disconnect();
			}
			isConnected = false;
		};
		ws.onmessage = (e) => {
			//当接收到的数据为数据流我们才进行播放
			if (e.data instanceof Blob) {
				listen(e.data);
			}
		};
	} catch (e) {
		console.error('无效地址');
	}
};

发送数据

js
const sendData = () => {
	if (size !== 0) {
		//二进制文件通过ws上传到服务器
		const pcmData = encodePCM();
		if (pcmData) {
			// const blob = new Blob([pcmData]);
			//如果websocket连接成功则发送消息
			if (isConnected) {
				ws.send(pcmData);
			}
		}
	}
};

接收数据

js
/**
 * @param data 二进制数据流(Blob)
 */
const listen = (data) => {
	receiveData.push(data);
	//25包为一秒长度的音频
	if (receiveData.length === 25) {
		const totalData = new Blob(receiveData);
		//通过FileReader来读取相关数据
		const reader = new FileReader();

		reader.readAsArrayBuffer(totalData);
		reader.onload = function () {
			//af是ArrayBuffer字节码类型,af.byteLength是后台返回的字节总数
			const af = reader.result;
			//ArrayBuffer是二进制数据,不能直接操作,用DataView(视图)去操作.
			const data = new DataView(af);

			const currentPlayer = document.createElement('audio');

			currentPlayer.src = window.URL.createObjectURL(encodeWAV(data));
			currentPlayer.play().catch((error) => {
				console.error('播放失败');
			});
		};
		//清空旧数据
		receiveData = [];
	}
};
/**
 * @param pcmData DataView
 */
const encodeWAV = (pcmData) => {
	const sampleRate = 8000;
	const sampleBits = 16;
	const dataLength = pcmData.byteLength;
	const buffer = new ArrayBuffer(44 + dataLength);
	const data = new DataView(buffer);

	const channelCount = 1; //单声道
	let offset = 0;

	const writeString = function (str) {
		for (let i = 0; i < str.length; i++) {
			data.setUint8(offset + i, str.charCodeAt(i));
		}
	};

	// 资源交换文件标识符
	writeString('RIFF');
	offset += 4;
	// 下个地址开始到文件尾总字节数,即文件大小-8
	data.setUint32(offset, 36 + dataLength, true);
	offset += 4;
	// WAV文件标志
	writeString('WAVE');
	offset += 4;
	// 波形格式标志
	writeString('fmt ');
	offset += 4;
	// 过滤字节,一般为 0x10 = 16
	data.setUint32(offset, 16, true);
	offset += 4;
	// 格式类别 (PCM形式采样数据)
	// data.setUint16(offset, 1, true); offset += 2;
	// 格式类别 (G.711 a-law) wav支持多种压缩编码格式
	data.setUint16(offset, 6, true);
	offset += 2;
	// 通道数
	data.setUint16(offset, channelCount, true);
	offset += 2;
	// 采样率,每秒样本数,表示每个通道的播放速度
	data.setUint32(offset, sampleRate, true);
	offset += 4;
	// 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
	data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true);
	offset += 4;
	// 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
	data.setUint16(offset, channelCount * (sampleBits / 8), true);
	offset += 2;
	// 每样本数据位数
	data.setUint16(offset, sampleBits, true);
	offset += 2;
	// 数据标识符
	writeString('data');
	offset += 4;
	// 采样数据总数,即数据总大小-44
	data.setUint32(offset, dataLength, true);
	offset += 4;

	// 数据体
	// 写入后台传来的采样数据,直接一个字节一个字节写入
	for (let i = 0; i < dataLength; i++) {
		data.setInt8(offset, pcmData.getInt8(i));
		offset++;
	}

	return new Blob([data], { type: 'audio/wav' });
};

最后我们只需要调用 init() initWs() 即可 完整代码

贡献者

暂无相关贡献者

变更记录

暂无最近变更历史

Released under the MIT License.