Created
November 14, 2018 14:30
-
-
Save rayyee/ffa9cdadda3cb58e752832464d865c68 to your computer and use it in GitHub Desktop.
改良HZRecorder.js的采样压缩
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function (window) { | |
//兼容 | |
window.URL = window.URL || window.webkitURL; | |
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; | |
//**blob to dataURL** | |
var blobToDataURL = function (blob, callback) { | |
var a = new FileReader(); | |
a.onload = function (e) { callback(e.target.result); } | |
a.readAsDataURL(blob); | |
} | |
/** 降低采样率 @rate 新的采样率 @sampleRate 现在的采样率 **/ | |
function downsampleBuffer(buffer, rate, sampleRate) { | |
if (rate == sampleRate) { | |
return buffer; | |
} | |
if (rate > sampleRate) { | |
throw "downsampling rate show be smaller than original sample rate"; | |
} | |
var sampleRateRatio = sampleRate / rate; | |
var newLength = Math.round(buffer.length / sampleRateRatio); | |
var result = new Float32Array(newLength); | |
var offsetResult = 0; | |
var offsetBuffer = 0; | |
while (offsetResult < result.length) { | |
var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio); | |
var accum = 0, count = 0; | |
for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) { | |
accum += buffer[i]; | |
count++; | |
} | |
result[offsetResult] = accum / count; | |
offsetResult++; | |
offsetBuffer = nextOffsetBuffer; | |
} | |
return result; | |
} | |
var HZRecorder = function (stream, config) { | |
config = config || {}; | |
config.sampleBits = config.sampleBits || 16; //采样数位 8, 16 | |
config.sampleRate = config.sampleRate || (16000); //采样率(1/6 44100) | |
var context = new AudioContext(); | |
var audioInput = context.createMediaStreamSource(stream); | |
var recorder = context.createScriptProcessor(4096, 1, 1); | |
var audioData = { | |
size: 0 //录音文件长度 | |
, buffer: [] //录音缓存 | |
, inputSampleRate: context.sampleRate //输入采样率 | |
, inputSampleBits: 16 //输入采样数位 8, 16 | |
, outputSampleRate: config.sampleRate //输出采样率 | |
, oututSampleBits: config.sampleBits //输出采样数位 8, 16 | |
, input: function (data) { | |
this.buffer.push(new Float32Array(data)); | |
this.size += data.length; | |
} | |
, compress: function () { //合并压缩 | |
//合并 | |
var data = new Float32Array(this.size); | |
var offset = 0; | |
for (var i = 0; i < this.buffer.length; i++) { | |
data.set(this.buffer[i], offset); | |
offset += this.buffer[i].length; | |
} | |
//压缩 | |
return downsampleBuffer(data, this.outputSampleRate, this.inputSampleRate); | |
/** 以下的压缩方法会导致声音变慢,使用downsampleBuffer来降低采样率 **/ | |
// var compression = parseInt(this.inputSampleRate / this.outputSampleRate); | |
// var length = data.length / compression; | |
// var result = new Float32Array(length); | |
// var index = 0, j = 0; | |
// while (index < length) { | |
// result[index] = data[j]; | |
// j += compression; | |
// index++; | |
// } | |
// return result; | |
} | |
, encodeWAV: function () { | |
var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate); | |
var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits); | |
var bytes = this.compress(); | |
var dataLength = bytes.length * (sampleBits / 8); | |
var buffer = new ArrayBuffer(44 + dataLength); | |
var data = new DataView(buffer); | |
var channelCount = 1;//单声道 | |
var offset = 0; | |
var writeString = function (str) { | |
for (var 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; | |
// 通道数 | |
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; | |
// 写入采样数据 | |
if (sampleBits === 8) { | |
for (var i = 0; i < bytes.length; i++, offset++) { | |
var s = Math.max(-1, Math.min(1, bytes[i])); | |
var val = s < 0 ? s * 0x8000 : s * 0x7FFF; | |
val = parseInt(255 / (65535 / (val + 32768))); | |
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])); | |
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); | |
} | |
} | |
return new Blob([data], { type: 'audio/wav' }); | |
} | |
}; | |
//开始录音 | |
this.start = function () { | |
audioInput.connect(recorder); | |
recorder.connect(context.destination); | |
} | |
//停止 | |
this.stop = function () { | |
recorder.disconnect(); | |
} | |
//获取音频文件 | |
this.getBlob = function () { | |
this.stop(); | |
return audioData.encodeWAV(); | |
} | |
//回放 | |
this.play = function (audio) { | |
audio.src = window.URL.createObjectURL(this.getBlob()); | |
} | |
//上传 | |
this.upload = function (url, callback) { | |
blobToDataURL(this.getBlob(), function(dataUrlBase){ | |
let audioBase64 = dataUrlBase.split(',')[1]; | |
fetch('//your-server-domain.com/post-audio', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({audioBase64}) | |
}); | |
}); | |
} | |
//音频采集 | |
recorder.onaudioprocess = function (e) { | |
audioData.input(e.inputBuffer.getChannelData(0)); | |
//record(e.inputBuffer.getChannelData(0)); | |
} | |
}; | |
//抛出异常 | |
HZRecorder.throwError = function (message) { | |
alert(message); | |
throw new function () { this.toString = function () { return message; } } | |
} | |
//是否支持录音 | |
HZRecorder.canRecording = (navigator.getUserMedia != null); | |
//获取录音机 | |
HZRecorder.get = function (callback, config) { | |
if (callback) { | |
if (navigator.getUserMedia) { | |
navigator.getUserMedia( | |
{ audio: true } //只启用音频 | |
, function (stream) { | |
var rec = new HZRecorder(stream, config); | |
callback(rec); | |
} | |
, function (error) { | |
switch (error.code || error.name) { | |
case 'PERMISSION_DENIED': | |
case 'PermissionDeniedError': | |
HZRecorder.throwError('用户拒绝提供信息。'); | |
break; | |
case 'NOT_SUPPORTED_ERROR': | |
case 'NotSupportedError': | |
HZRecorder.throwError('浏览器不支持硬件设备。'); | |
break; | |
case 'MANDATORY_UNSATISFIED_ERROR': | |
case 'MandatoryUnsatisfiedError': | |
HZRecorder.throwError('无法发现指定的硬件设备。'); | |
break; | |
default: | |
console.log('无法打开麦克风。异常信息:', error); | |
HZRecorder.throwError('无法打开麦克风。异常信息:' + (error.code || error.name)); | |
break; | |
} | |
}); | |
} else { | |
HZRecorder.throwErr('当前浏览器不支持录音功能。'); return; | |
} | |
} | |
} | |
window.HZRecorder = HZRecorder; | |
})(window); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
重新采样有许多方法只是简单的做插值计算就可以,
但对于像 44100 / 16000 = 2.75625 这样的浮点数factor,这种情况需要更复杂的算法。
解决方案来自于
https://stackoverflow.com/a/26245260/9286894
其他可能的解决方案
https://stackoverflow.com/a/27601521/9286894
https://stackoverflow.com/a/30032095/9286894
https://github.com/taisel/XAudioJS/blob/master/resampler.js