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>

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
要将PCM数据流封装WAV文件格式,需要将PCM数据流保存到WAV文件中,并为WAV文件添加相应的文件头和元数据。WAV文件格式是一种常见的音频文件格式,通常用于存储未经压缩的音频数据。 下面是一个简单的示例代码,演示了如何将PCM数据流封装WAV文件格式: ```java public static void pcmToWav(String pcmPath, String wavPath, int sampleRate, int channels, int bitPerSample) { FileInputStream pcmInputStream = null; FileOutputStream wavOutputStream = null; long totalAudioLen, totalDataLen; long longSampleRate = sampleRate; int byteRate = bitPerSample * channels * (int) (longSampleRate / 8); try { pcmInputStream = new FileInputStream(new File(pcmPath)); wavOutputStream = new FileOutputStream(new File(wavPath)); totalAudioLen = pcmInputStream.getChannel().size(); totalDataLen = totalAudioLen + 36; byte[] data = new byte[1024]; writeWaveFileHeader(wavOutputStream, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate); while (pcmInputStream.read(data) != -1) { wavOutputStream.write(data); } pcmInputStream.close(); wavOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } private static void writeWaveFileHeader( FileOutputStream outputStream, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, int byteRate) throws IOException { byte[] header = new byte[44]; header[0] = 'R'; // RIFF/WAVE header header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; header[4] = (byte) (totalDataLen & 0xff); header[5] = (byte) ((totalDataLen >> 8) & 0xff); header[6] = (byte) ((totalDataLen >> 16) & 0xff); header[7] = (byte) ((totalDataLen >> 24) & 0xff); header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; header[12] = 'f'; // 'fmt ' chunk header[13] = 'm'; header[14] = 't'; header[15] = ' '; header[16] = 16; // 4 bytes: size of 'fmt ' chunk header[17] = 0; header[18] = 0; header[19] = 0; header[20] = 1; // format = 1 header[21] = 0; header[22] = (byte) channels; header[23] = 0; header[24] = (byte) (longSampleRate & 0xff); header[25] = (byte) ((longSampleRate >> 8) & 0xff); header[26] = (byte) ((longSampleRate >> 16) & 0xff); header[27] = (byte) ((longSampleRate >> 24) & 0xff); header[28] = (byte) (byteRate & 0xff); header[29] = (byte) ((byteRate >> 8) & 0xff); header[30] = (byte) ((byteRate >> 16) & 0xff); header[31] = (byte) ((byteRate >> 24) & 0xff); header[32] = (byte) (2 * 16 / 8); // block align header[33] = 0; header[34] = (byte) bitPerSample; header[35] = 0; header[36] = 'd'; // data chunk header[37] = 'a'; header[38] = 't'; header[39] = 'a'; header[40] = (byte) (totalAudioLen & 0xff); header[41] = (byte) ((totalAudioLen >> 8) & 0xff); header[42] = (byte) ((totalAudioLen >> 16) & 0xff); header[43] = (byte) ((totalAudioLen >> 24) & 0xff); outputStream.write(header, 0, 44); } ``` 在这个示例代码中,我们定义了一个`pcmToWav()`方法,用于将PCM数据流保存为WAV文件。该方法接收三个参数:PCM文件路径、WAV文件路径、采样率、声道数和位宽。在方法中,我们使用FileInputStream读取PCM文件中的数据,并使用FileOutputStream将WAV文件写入磁盘。同时,我们调用了`writeWaveFileHeader()`方法,向WAV文件中添加WAV文件头和元数据。在添加WAV文件头和元数据时,我们需要根据采样率、声道数和位宽计算出相应的文件头和元数据,并将其写入WAV文件中。 通过调用`pcmToWav()`方法,我们可以将PCM数据流保存为WAV文件,并在保存的过程中添加相应的文件头和元数据。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值