js实现pcm音频转wav与播放

js实现pcm音频转wav与播放

示例一:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>pcmtowav</title>
</head>
<body>
<div>
    getUserMedia需要https,使用localhost或127.0.0.1时,可用http。
</div>
<button id="start">开始录音</button>
<button id="end">结束录音</button>
<button id="play">播放录音</button>
</body>
<script>
    var context = null,
        inputData = [],
        size = 0,
        audioInput = null,
        recorder = null,
        dataArray;

    document.getElementById('start').addEventListener('click', function() {
        context = new (window.AudioContext || window.webkitAudioContext)();
        // 清空数据
        inputData = [];
        // 录音节点
        recorder = context.createScriptProcessor(4096, 1, 1);

        recorder.onaudioprocess = function(e) {
            var data = e.inputBuffer.getChannelData(0);

            inputData.push(new Float32Array(data));
            size += data.length;
        }

        navigator.mediaDevices.getUserMedia({
            audio: true
        }).then((stream) => {
            audioInput = context.createMediaStreamSource(stream);

        }).catch((err) => {
            console.log('error');
        }).then(function() {
            audioInput.connect(recorder);
            recorder.connect(context.destination);
        });
    });
    document.getElementById('end').addEventListener('click', function() {
        recorder.disconnect();
    });
    document.getElementById('play').addEventListener('click', function() {
        recorder.disconnect();
        if (0 !== size) {
            // 组合数据
            // var data = combine(inputData, size);
            inputSampleRate = context.sampleRate;
            context.decodeAudioData(encodeWAV().buffer, function(buffer) {
                // decodeAudioData,是支持promise,三参数的知识兼容老的
                playSound(buffer);
            }, function() {
                console.log('error');
            });
            // console.log(data.buffer);
        }
    });
    // ----------------------
    // 以下是增加的内容

    var inputSampleRate = 0;   // 输入采样率
    var oututSampleBits = 16;  // 输出采样数位

    // 数据简单处理
    function decompress() {
        // 合并
        var data = new Float32Array(size);
        var offset = 0; // 偏移量计算
        // 将二维数据,转成一维数据
        for (var i = 0; i < inputData.length; i++) {
            data.set(inputData[i], offset);
            offset += inputData[i].length;
        }
        return data;
    };
    function encodePCM() {
        let bytes = decompress(),
            sampleBits = oututSampleBits,
            offset = 0,
            dataLength = bytes.length * (sampleBits / 8),
            buffer = new ArrayBuffer(dataLength),
            data = new DataView(buffer);

        // 写入采样数据
        if (sampleBits === 8) {
            for (var i = 0; i < bytes.length; i++, offset++) {
                // 范围[-1, 1]
                var s = Math.max(-1, Math.min(1, bytes[i]));
                // 8位采样位划分成2^8=256份,它的范围是0-255; 16位的划分的是2^16=65536份,范围是-32768到32767
                // 因为我们收集的数据范围在[-1,1],那么你想转换成16位的话,只需要对负数*32768,对正数*32767,即可得到范围在[-32768,32767]的数据。
                // 对于8位的话,负数*128,正数*127,然后整体向上平移128(+128),即可得到[0,255]范围的数据。
                var val = s < 0 ? s * 128 : s * 127;
                val = parseInt(val + 128);
                data.setInt8(offset, val, true);
            }
        } else {
            for (var i = 0; i < bytes.length; i++, offset += 2) {
                var s = Math.max(-1, Math.min(1, bytes[i]));
                // 16位直接乘就行了
                data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
            }
        }

        return data;
    }

    function encodeWAV() {
        var sampleRate = inputSampleRate;
        var sampleBits = oututSampleBits;
        var bytes = encodePCM();
        var buffer = new ArrayBuffer(44 + bytes.byteLength);
        var data = new DataView(buffer);

        var channelCount = 1;   // 单声道
        var offset = 0;

        // 资源交换文件标识符
        writeString(data, offset, 'RIFF'); offset += 4;
        // 下个地址开始到文件尾总字节数,即文件大小-8
        data.setUint32(offset, 36 + bytes.byteLength, true); offset += 4;
        // WAV文件标志
        writeString(data, offset, 'WAVE'); offset += 4;
        // 波形格式标志
        writeString(data, offset, 'fmt '); offset += 4;
        // 过滤字节,一般为 0x10 = 16
        data.setUint32(offset, 16, true); offset += 4;
        // 格式类别 (PCM形式采样数据)
        data.setUint16(offset, 1, 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, 'data'); offset += 4;
        // 采样数据总数,即数据总大小-44
        data.setUint32(offset, bytes.byteLength, true); offset += 4;

        // 给wav头增加pcm体
        for (let i = 0; i < bytes.byteLength; ++i) {
            data.setUint8(offset, bytes.getUint8(i, true), true);
            offset++;
        }

        return data;
    }

    function getWAVBlob() {
        return new Blob([ encodeWAV() ], { type: 'audio/wav' });
    }
    function playSound(buffer) {
        var source = context.createBufferSource();

        // 设置数据
        source.buffer = buffer;
        // connect到扬声器
        source.connect(context.destination);
        source.start();
    }

    function writeString(data, offset, str) {
        for (var i = 0; i < str.length; i++) {
            data.setUint8(offset + i, str.charCodeAt(i));
        }
    }
    function combineDataView(resultConstructor, ...arrays) {
        let totalLength = 0,
            offset = 0;
        // 统计长度
        for (let arr of arrays) {
            totalLength += arr.length || arr.byteLength;
        }
        // 创建新的存放变量
        let buffer = new ArrayBuffer(totalLength),
            result = new resultConstructor(buffer);
        // 设置数据
        for (let arr of arrays) {
            // dataview合并
            for (let i = 0, len = arr.byteLength; i < len; ++i) {
                result.setInt8(offset, arr.getInt8(i));
                offset += 1;
            }
        }

        return result;
    }
</script>
</html>

示例二:

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="description" content="浏览器捕捉麦克风功能测试1">
    <title>浏览器捕捉麦克风功能测试1</title>
    <style>
        table {
            margin: 20px auto;
            border-spacing: 0;
            border-collapse: collapse;
        }

        th, td {
            padding: 6px 13px;
            border: 1px solid #dfe2e5;
        }
    </style>
</head>
<body>
    <div style="text-align: center;">
        <h1>浏览器捕捉麦克风功能测试1</h1>
        <hr/>
        <table>
            <tbody>
            <tr>
                <th>播放麦克风采集的原始音频流</th>
                <td>
                    <button onclick="play()">播放</button>
                    <button onclick="pause()">暂停</button>
                </td>
            </tr>
            <tr>
                <th>录音+播放的形式来模拟连续的声音</th>
                <td>
                    <button onclick="play2()">播放</button>
                    <button onclick="pause2()">暂停</button>
                </td>
            </tr>
            </tbody>
        </table>
    </div>
</body>
<script type="text/javascript" src="js/media.js"></script>
<script type="text/javascript">
    var microPhone = new MicroPhone();
    var audioContext = new AudioContext();

    /**
     * 播放麦克风采集的原始音频流
     */
    function play() {
        microPhone.start();
    }

    function pause() {
        microPhone.stop();
    }

    /**
     * 录音+播放的形式来模拟连续的声音
     */
    function play2() {
        if (!microPhone.isRecording) {
            microPhone.startRecord({}, function(samples) {
                playData1(samples);
                // 方式二
                // playData2(samples);
            });
        }
    }

    /**
     * 播放原始音频数据
     * @param inputBuffer
     */
    function playData1(inputBuffer) {
        audioContext.playBuffer(inputBuffer);
    }

    /**
     * 使用AudioContext提供的解码接口播放编码后的数据
     * 此处只做调用示范,不推荐使用
     * @param inputBuffer
     */
    function playData2(inputBuffer) {
        var channelCount = inputBuffer.numberOfChannels,
            length = inputBuffer.length,
            samples = new Float32Array(channelCount * length);
        for (var i = 0; i < length; i++) {
            for (var j = 0; j < channelCount; j++) {
                samples[i * channelCount + j] = inputBuffer.getChannelData(j)[i];
            }
        }
        // 先编码成wav
        var wav = encodeWAV(samples, microPhone.recorderConfig);
        // 再解码
        audioContext.decodeAudioData(wav, function(buffer) {
            audioContext.playBuffer(buffer);
        });
    }

    function pause2() {
        if (microPhone.isRecording) {
            microPhone.stopRecord();
        }
    }
</script>
</html>
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳宗德

您的鼓励是我进步的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值