js 采集pcm,并封装为wav,包含重采样,提供下载

<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>录制音频并下载为WAV文件</title>

</head>

<body>

<button id="startButton">开始录制</button>

<button id="stopButton" disabled>停止录制</button>

<a id="downloadLink" style="display: none">下载录音</a>

<script>

// 创建一个音频上下文

//let mediaRecorder;

const recordedChunks = [];

resample = 48000

document.getElementById('startButton').addEventListener('click', () => {

navigator.mediaDevices.getUserMedia({ audio: true })

.then((stream) => {

let track = stream.getAudioTracks()[0];

console.log(track.getCapabilities());

const audioContext = new (window.AudioContext || window.webkitAudioContext)();

console.log("adadadad")

// 将麦克风输入连接到音频上下文

const microphone = audioContext.createMediaStreamSource(stream);

// 创建一个ScriptProcessorNode来处理音频数据

const scriptNode = audioContext.createScriptProcessor(4096, 1, 1);

// 监听ScriptProcessorNode的audioprocess事件

scriptNode.onaudioprocess = (event) => {

// 获取PCM数据

const inputBuffer = event.inputBuffer;

const pcmData = inputBuffer.getChannelData(0); // 获取单个声道的PCM数据

//console.log(pcmData)

// 存储PCM数据块

var data = interpolateArray(new Float32Array(pcmData), 16000, audioContext.sampleRate)

//console.log(data)

recordedChunks.push(data);

//recordedChunks.push(new Float32Array(pcmData))

};

// 连接音频节点

microphone.connect(scriptNode);

scriptNode.connect(audioContext.destination);

// 创建MediaRecorder并开始录制

//mediaRecorder = new MediaRecorder(stream);

//mediaRecorder.ondataavailable = (event) => {

// if (event.data.size > 0) {

// recordedChunks.push(event.data);

// }

//};

//mediaRecorder.onstop = () =>

//mediaRecorder.start();

document.getElementById('startButton').disabled = true;

document.getElementById('stopButton').disabled = false;

})

.catch((error) => {

console.error('获取麦克风权限失败:', error);

});

});

document.getElementById('stopButton').addEventListener('click', () => {

console.log('停止录制');

console.log(recordedChunks)

const pcmData = flattenArray(recordedChunks);

console.log(pcmData)

// 创建WAV文件头

const wavHeader = createWavHeader(pcmData.byteLength, 16000);

// 合并WAV文件头和PCM数据

const wavBlob = new Blob([wavHeader, pcmData], { type: 'audio/wav' });

//const wavBlob = new Blob([pcmData], { type: 'audio/pcm' });

// 创建下载链接

const downloadLink = document.getElementById('downloadLink');

downloadLink.href = URL.createObjectURL(wavBlob);

downloadLink.download = 'recorded.wav';

downloadLink.style.display = 'block';

document.getElementById('startButton').disabled = false;

document.getElementById('stopButton').disabled = true;

//if (mediaRecorder.state === 'recording') {

// mediaRecorder.stop();

//}

});

// for changing the sampling rate, data,

function interpolateArray(data, newSampleRate, oldSampleRate) {

var fitCount = Math.round(data.length*(newSampleRate/oldSampleRate));

var newData = new Array();

var springFactor = new Number((data.length - 1) / (fitCount - 1));

newData[0] = data[0]; // for new allocation

for ( var i = 1; i < fitCount - 1; i++) {

var tmp = i * springFactor;

var before = new Number(Math.floor(tmp)).toFixed();

var after = new Number(Math.ceil(tmp)).toFixed();

var atPoint = tmp - before;

newData[i] = this.linearInterpolate(data[before], data[after], atPoint);

}

newData[fitCount - 1] = data[data.length - 1]; // for new allocation

return newData;

};

function linearInterpolate(before, after, atPoint) {

return before + (after - before) * atPoint;

};

// 辅助函数:将二维数组扁平化为一维数组

function flattenArray(arrays, sampleRate) {

const buffer = new ArrayBuffer(arrays.length * 4096*2);

const view = new DataView(buffer);

let offset = 0

for (var i = 0; i < arrays.length; i++)

{

for (var j = 0; j < arrays[i].length; j++)

{

data = parseInt(arrays[i][j] * 32768)

view.setUint16(offset, data, true);

offset += 2

}

}

return buffer.slice(0, offset)

}

// 辅助函数:创建WAV文件头

function createWavHeader(dataSize, sampleRate) {

const buffer = new ArrayBuffer(44);

const view = new DataView(buffer);

// Chunk ID

view.setUint32(0, 0x52494646, false); // "RIFF"

// File size (excluding first 8 bytes)

console.log(dataSize)

view.setUint32(4, dataSize + 36, true);

// Format (WAVE)

view.setUint32(8, 0x57415645, false); // "WAVE"

// Subchunk 1 ID (fmt)

view.setUint32(12, 0x666D7420, false); // "fmt "

// Subchunk 1 size

view.setUint32(16, 16, true);

// Audio format (PCM)

view.setUint16(20, 1, true);

// Number of channels (1 for mono)

view.setUint16(22, 1, true);

// Sample rate

view.setUint32(24, sampleRate, true);

// Byte rate (sample rate * block align)

view.setUint32(28, sampleRate * 2, true);

// Block align (number of bytes per sample)

view.setUint16(32, 2, true);

// Bits per sample

view.setUint16(34, 16, true);

// Subchunk 2 ID (data)

view.setUint32(36, 0x64617461, false); // "data"

// Subchunk 2 size

view.setUint32(40, dataSize, true);

return buffer;

}

</script>

</body>

</html>

相关推荐
Watermelo6172 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248943 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356115 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript