This repository has been archived on 2025-05-21. You can view files and clone it, but cannot push or open issues or pull requests.
2025-04-20 18:46:48 +08:00

1155 lines
38 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
录音 Recorder扩展音频变速变调转换本代码从Sonic.java移植
https://github.com/xiangyuecn/Recorder
Recorder.Sonic(set)
Recorder.Sonic.Async(set)
有两种构造方法Sonic是同步方法Sonic.Async是异步方法同步方法简单直接但处理量大时会消耗大量时间主要用于一次性的处理异步方法由WebWorker在后台进行运算处理但异步方法不一定能成功开启低版本浏览器主要用于实时处理。
注意异步方法调用后必须调用flush方法否则会产生内存泄露。
【构造初始化参数】
set:{
sampleRate:待处理pcm的采样率就是input输入的buffer的采样率
}
【功能配置调用函数】同步异步通用以下num取值正常为0.1-2.0,超过这个范围也是可以的,但不推荐
.setPitch(num) num:0.1-n变调不变速会说话的汤姆猫男女变声只调整音调不改变播放速度默认为1.0不调整
.setSpeed(num) num:0.1-n变速不变调快放慢放只调整播放速度不改变音调默认为1.0不调整
.setRate(num) num:0.1-n变速变调越小越缓重越大越尖锐会改变播放速度和音调默认为1.0不调整
.setVolume(num) num:0.1-n调整音量默认为1.0不调整
.setChordPitch(bool) bool:默认false作用未知不推荐使用
.setQuality(num) num:0或1默认0时会减小输入采样率来提供处理速度变调时才会用到不推荐使用
【同步调用方法】
.input(buffer) buffer:[Int16,...] 一维数组输入pcm数据返回转换后的部分pcm数据完整输出需要调用flush返回值[Int16,...]长度可能为0代表没有数据被转换此方法是耗时的方法一次性处理大量pcm需要切片+setTimeout优化
.flush() 将残余的未转换的pcm数据完成转换并返回返回值[Int16,...]长度可能为0代表没有数据被转换flush后不能再调用input
【异步调用方法】
.input(buffer,callback) callback:fn(pcm)和同步方法相同只是返回值通过callback返回
.flush(callback) callback:fn(pcm)和同步方法相同只是返回值通过callback返回
*/
(function(factory){
var browser=typeof window=="object" && !!window.document;
var win=browser?window:Object; //非浏览器环境Recorder挂载在Object下面
var rec=win.Recorder,ni=rec.i18n;
factory(rec,ni,ni.$T,browser);
}(function(Recorder,i18n,$T,isBrowser){
"use strict";
//是否支持web worker
var HasWebWorker=isBrowser && typeof Worker=="function";
function SonicFunction(SonicFunction_set){//用函数包裹方便Web Worker化
//暴露接口
var fn=function(set){
this.set=set;
var sonic=Sonic_Class(this);
this.sonic=sonic;
sonic.New(set.sampleRate,1);
};
fn.prototype=SonicFunction.prototype={
input:function(buffer){
this.sonic.writeShortToStream(buffer);
return this.sonic.readShortFromStream();
}
,flush:function(){
this.sonic.flushStream();
return this.sonic.readShortFromStream();
}
};
//java 兼容环境
var System={
arraycopy:function(src,srcPos,dest,destPos,len){
for(var i=0;i<len;i++){
dest[destPos+i]=src[srcPos+i];
};
}
};
/* Sonic library
Copyright 2010, 2011
Bill Cox
This file is part of the Sonic Library.
This file is licensed under the Apache 2.0 license.
https://github.com/waywardgeek/sonic/blob/71c51195de71627d7443d05378c680ba756545e8/Sonic.java
*/
//Sonic.java 转写 js
function Sonic_Class(FnObj) {
var SONIC_MIN_PITCH = 65;
var SONIC_MAX_PITCH = 400;
// This is used to down-sample some inputs to improve speed
var SONIC_AMDF_FREQ = 4000;
// The number of points to use in the sinc FIR filter for resampling.
var SINC_FILTER_POINTS = 12;
var SINC_TABLE_SIZE = 601;
// Lookup table for windowed sinc function of SINC_FILTER_POINTS points.
var sincTable = [
0, 0, 0, 0, 0, 0, 0, -1, -1, -2, -2, -3, -4, -6, -7, -9, -10, -12, -14,
-17, -19, -21, -24, -26, -29, -32, -34, -37, -40, -42, -44, -47, -48, -50,
-51, -52, -53, -53, -53, -52, -50, -48, -46, -43, -39, -34, -29, -22, -16,
-8, 0, 9, 19, 29, 41, 53, 65, 79, 92, 107, 121, 137, 152, 168, 184, 200,
215, 231, 247, 262, 276, 291, 304, 317, 328, 339, 348, 357, 363, 369, 372,
374, 375, 373, 369, 363, 355, 345, 332, 318, 300, 281, 259, 234, 208, 178,
147, 113, 77, 39, 0, -41, -85, -130, -177, -225, -274, -324, -375, -426,
-478, -530, -581, -632, -682, -731, -779, -825, -870, -912, -951, -989,
-1023, -1053, -1080, -1104, -1123, -1138, -1149, -1154, -1155, -1151,
-1141, -1125, -1105, -1078, -1046, -1007, -963, -913, -857, -796, -728,
-655, -576, -492, -403, -309, -210, -107, 0, 111, 225, 342, 462, 584, 708,
833, 958, 1084, 1209, 1333, 1455, 1575, 1693, 1807, 1916, 2022, 2122, 2216,
2304, 2384, 2457, 2522, 2579, 2625, 2663, 2689, 2706, 2711, 2705, 2687,
2657, 2614, 2559, 2491, 2411, 2317, 2211, 2092, 1960, 1815, 1658, 1489,
1308, 1115, 912, 698, 474, 241, 0, -249, -506, -769, -1037, -1310, -1586,
-1864, -2144, -2424, -2703, -2980, -3254, -3523, -3787, -4043, -4291,
-4529, -4757, -4972, -5174, -5360, -5531, -5685, -5819, -5935, -6029,
-6101, -6150, -6175, -6175, -6149, -6096, -6015, -5905, -5767, -5599,
-5401, -5172, -4912, -4621, -4298, -3944, -3558, -3141, -2693, -2214,
-1705, -1166, -597, 0, 625, 1277, 1955, 2658, 3386, 4135, 4906, 5697, 6506,
7332, 8173, 9027, 9893, 10769, 11654, 12544, 13439, 14335, 15232, 16128,
17019, 17904, 18782, 19649, 20504, 21345, 22170, 22977, 23763, 24527,
25268, 25982, 26669, 27327, 27953, 28547, 29107, 29632, 30119, 30569,
30979, 31349, 31678, 31964, 32208, 32408, 32565, 32677, 32744, 32767,
32744, 32677, 32565, 32408, 32208, 31964, 31678, 31349, 30979, 30569,
30119, 29632, 29107, 28547, 27953, 27327, 26669, 25982, 25268, 24527,
23763, 22977, 22170, 21345, 20504, 19649, 18782, 17904, 17019, 16128,
15232, 14335, 13439, 12544, 11654, 10769, 9893, 9027, 8173, 7332, 6506,
5697, 4906, 4135, 3386, 2658, 1955, 1277, 625, 0, -597, -1166, -1705,
-2214, -2693, -3141, -3558, -3944, -4298, -4621, -4912, -5172, -5401,
-5599, -5767, -5905, -6015, -6096, -6149, -6175, -6175, -6150, -6101,
-6029, -5935, -5819, -5685, -5531, -5360, -5174, -4972, -4757, -4529,
-4291, -4043, -3787, -3523, -3254, -2980, -2703, -2424, -2144, -1864,
-1586, -1310, -1037, -769, -506, -249, 0, 241, 474, 698, 912, 1115, 1308,
1489, 1658, 1815, 1960, 2092, 2211, 2317, 2411, 2491, 2559, 2614, 2657,
2687, 2705, 2711, 2706, 2689, 2663, 2625, 2579, 2522, 2457, 2384, 2304,
2216, 2122, 2022, 1916, 1807, 1693, 1575, 1455, 1333, 1209, 1084, 958, 833,
708, 584, 462, 342, 225, 111, 0, -107, -210, -309, -403, -492, -576, -655,
-728, -796, -857, -913, -963, -1007, -1046, -1078, -1105, -1125, -1141,
-1151, -1155, -1154, -1149, -1138, -1123, -1104, -1080, -1053, -1023, -989,
-951, -912, -870, -825, -779, -731, -682, -632, -581, -530, -478, -426,
-375, -324, -274, -225, -177, -130, -85, -41, 0, 39, 77, 113, 147, 178,
208, 234, 259, 281, 300, 318, 332, 345, 355, 363, 369, 373, 375, 374, 372,
369, 363, 357, 348, 339, 328, 317, 304, 291, 276, 262, 247, 231, 215, 200,
184, 168, 152, 137, 121, 107, 92, 79, 65, 53, 41, 29, 19, 9, 0, -8, -16,
-22, -29, -34, -39, -43, -46, -48, -50, -52, -53, -53, -53, -52, -51, -50,
-48, -47, -44, -42, -40, -37, -34, -32, -29, -26, -24, -21, -19, -17, -14,
-12, -10, -9, -7, -6, -4, -3, -2, -2, -1, -1, 0, 0, 0, 0, 0, 0, 0
];
var inputBuffer;
var outputBuffer;
var pitchBuffer;
var downSampleBuffer;
var speed=0;
var volume=0;
var pitch=0;
var rate=0;
var oldRatePosition=0;
var newRatePosition=0;
var useChordPitch=false;
var quality=0;
var numChannels=0;
var inputBufferSize=0;
var pitchBufferSize=0;
var outputBufferSize=0;
var numInputSamples=0;
var numOutputSamples=0;
var numPitchSamples=0;
var minPeriod=0;
var maxPeriod=0;
var maxRequired=0;
var remainingInputToCopy=0;
var sampleRate=0;
var prevPeriod=0;
var prevMinDiff=0;
var minDiff=0;
var maxDiff=0;
// Resize the array.
function resize(
oldArray,
newLength)
{
newLength *= numChannels;
var newArray = new Int16Array(newLength);
var length = oldArray.length <= newLength? oldArray.length : newLength;
System.arraycopy(oldArray, 0, newArray, 0, length);
return newArray;
}
// Move samples from one array to another. May move samples down within an array, but not up.
function move(
dest,
destPos,
source,
sourcePos,
numSamples)
{
System.arraycopy(source, sourcePos*numChannels, dest, destPos*numChannels, numSamples*numChannels);
}
// Scale the samples by the factor.
function scaleSamples(
samples,
position,
numSamples,
volume)
{
var fixedPointVolume = Math.floor(volume*4096.0);
var start = position*numChannels;
var stop = start + numSamples*numChannels;
for(var xSample = start; xSample < stop; xSample++) {
var value = (samples[xSample]*fixedPointVolume) >> 12;
if(value > 32767) {
value = 32767;
} else if(value < -32767) {
value = -32767;
}
samples[xSample] = value;
}
}
// Get the speed of the stream.
function getSpeed()
{
return speed;
}
// Set the speed of the stream.
function setSpeed(
speed_)
{
speed = speed_;
}
// Get the pitch of the stream.
function getPitch()
{
return pitch;
}
// Set the pitch of the stream.
function setPitch(
pitch_)
{
pitch = pitch_;
}
// Get the rate of the stream.
function getRate()
{
return rate;
}
// Set the playback rate of the stream. This scales pitch and speed at the same time.
function setRate(
rate_)
{
if(rate!=rate_){//允许任意设置
rate = rate_;
oldRatePosition = 0;
newRatePosition = 0;
}
}
// Get the vocal chord pitch setting.
function getChordPitch()
{
return useChordPitch;
}
// Set the vocal chord mode for pitch computation. Default is off.
function setChordPitch(
useChordPitch_)
{
useChordPitch = useChordPitch_;
}
// Get the quality setting.
function getQuality()
{
return quality;
}
// Set the "quality". Default 0 is virtually as good as 1, but very much faster.
function setQuality(
quality_)
{
quality = quality_;
}
// Get the scaling factor of the stream.
function getVolume()
{
return volume;
}
// Set the scaling factor of the stream.
function setVolume(
volume_)
{
volume = volume_;
}
// Allocate stream buffers.
function allocateStreamBuffers(
sampleRate_,
numChannels_)
{
minPeriod = Math.floor(sampleRate_/SONIC_MAX_PITCH);
maxPeriod = Math.floor(sampleRate_/SONIC_MIN_PITCH);
maxRequired = 2*maxPeriod;
inputBufferSize = maxRequired;
inputBuffer = new Int16Array(maxRequired*numChannels_);
outputBufferSize = maxRequired;
outputBuffer = new Int16Array(maxRequired*numChannels_);
pitchBufferSize = maxRequired;
pitchBuffer = new Int16Array(maxRequired*numChannels_);
downSampleBuffer = new Int16Array(maxRequired);
sampleRate = sampleRate_;
numChannels = numChannels_;
oldRatePosition = 0;
newRatePosition = 0;
prevPeriod = 0;
}
// Create a sonic stream.
function Sonic(
sampleRate,
numChannels)
{
allocateStreamBuffers(sampleRate, numChannels);
speed = 1.0;
pitch = 1.0;
volume = 1.0;
rate = 1.0;
oldRatePosition = 0;
newRatePosition = 0;
useChordPitch = false;
quality = 0;
}
// Get the sample rate of the stream.
function getSampleRate()
{
return sampleRate;
}
// Set the sample rate of the stream. This will cause samples buffered in the stream to be lost.
function setSampleRate(
sampleRate)
{
allocateStreamBuffers(sampleRate, numChannels);
}
// Get the number of channels.
function getNumChannels()
{
return numChannels;
}
// Set the num channels of the stream. This will cause samples buffered in the stream to be lost.
function setNumChannels(
numChannels)
{
allocateStreamBuffers(sampleRate, numChannels);
}
// Enlarge the output buffer if needed.
function enlargeOutputBufferIfNeeded(
numSamples)
{
if(numOutputSamples + numSamples > outputBufferSize) {
outputBufferSize += (outputBufferSize >> 1) + numSamples;
outputBuffer = resize(outputBuffer, outputBufferSize);
}
}
// Enlarge the input buffer if needed.
function enlargeInputBufferIfNeeded(
numSamples)
{
if(numInputSamples + numSamples > inputBufferSize) {
inputBufferSize += (inputBufferSize >> 1) + numSamples;
inputBuffer = resize(inputBuffer, inputBufferSize);
}
}
// Add the input samples to the input buffer.
function addShortSamplesToInputBuffer(
samples,
numSamples)
{
if(numSamples == 0) {
return;
}
enlargeInputBufferIfNeeded(numSamples);
move(inputBuffer, numInputSamples, samples, 0, numSamples);
numInputSamples += numSamples;
}
// Remove input samples that we have already processed.
function removeInputSamples(
position)
{
var remainingSamples = numInputSamples - position;
move(inputBuffer, 0, inputBuffer, position, remainingSamples);
numInputSamples = remainingSamples;
}
// Just copy from the array to the output buffer
function copyToOutput(
samples,
position,
numSamples)
{
enlargeOutputBufferIfNeeded(numSamples);
move(outputBuffer, numOutputSamples, samples, position, numSamples);
numOutputSamples += numSamples;
}
// Just copy from the input buffer to the output buffer. Return num samples copied.
function copyInputToOutput(
position)
{
var numSamples = remainingInputToCopy;
if(numSamples > maxRequired) {
numSamples = maxRequired;
}
copyToOutput(inputBuffer, position, numSamples);
remainingInputToCopy -= numSamples;
return numSamples;
}
// Read short data out of the stream. Sometimes no data will be available, and zero
// is returned, which is not an error condition.
function readShortFromStream() //已改成直接返回所有的Int16Array
{
var numSamples = numOutputSamples;
var samples=new Int16Array(numSamples);
var remainingSamples = 0;
if(numSamples == 0) {
return samples;
}
move(samples, 0, outputBuffer, 0, numSamples);
move(outputBuffer, 0, outputBuffer, numSamples, remainingSamples);
numOutputSamples = remainingSamples;
return samples;
}
// Force the sonic stream to generate output using whatever data it currently
// has. No extra delay will be added to the output, but flushing in the middle of
// words could introduce distortion.
function flushStream()
{
var remainingSamples = numInputSamples;
var s = speed/pitch;
var r = rate*pitch;
var expectedOutputSamples = Math.floor(numOutputSamples + Math.floor((remainingSamples/s + numPitchSamples)/r + 0.5));
// Add enough silence to flush both input and pitch buffers.
enlargeInputBufferIfNeeded(remainingSamples + 2*maxRequired);
for(var xSample = 0; xSample < 2*maxRequired*numChannels; xSample++) {
inputBuffer[remainingSamples*numChannels + xSample] = 0;
}
numInputSamples += 2*maxRequired;
writeShortToStream(null, 0);
// Throw away any extra samples we generated due to the silence we added.
if(numOutputSamples > expectedOutputSamples) {
numOutputSamples = expectedOutputSamples;
}
// Empty input and pitch buffers.
numInputSamples = 0;
remainingInputToCopy = 0;
numPitchSamples = 0;
}
// Return the number of samples in the output buffer
function samplesAvailable()
{
return numOutputSamples;
}
// If skip is greater than one, average skip samples together and write them to
// the down-sample buffer. If numChannels is greater than one, mix the channels
// together as we down sample.
function downSampleInput(
samples,
position,
skip)
{
var numSamples = Math.floor(maxRequired/skip);
var samplesPerValue = numChannels*skip;
var value;
position *= numChannels;
for(var i = 0; i < numSamples; i++) {
value = 0;
for(var j = 0; j < samplesPerValue; j++) {
value += samples[position + i*samplesPerValue + j];
}
value = Math.floor(value/samplesPerValue);
downSampleBuffer[i] = value;
}
}
// Find the best frequency match in the range, and given a sample skip multiple.
// For now, just find the pitch of the first channel.
function findPitchPeriodInRange(
samples,
position,
minPeriod,
maxPeriod)
{
var bestPeriod = 0, worstPeriod = 255;
var minDiff_ = 1, maxDiff_ = 0;
position *= numChannels;
for(var period = minPeriod; period <= maxPeriod; period++) {
var diff = 0;
for(var i = 0; i < period; i++) {
var sVal = samples[position + i];
var pVal = samples[position + period + i];
diff += sVal >= pVal? sVal - pVal : pVal - sVal;
}
/* Note that the highest number of samples we add into diff will be less
than 256, since we skip samples. Thus, diff is a 24 bit number, and
we can safely multiply by numSamples without overflow */
if(diff*bestPeriod < minDiff_*period) {
minDiff_ = diff;
bestPeriod = period;
}
if(diff*worstPeriod > maxDiff_*period) {
maxDiff_ = diff;
worstPeriod = period;
}
}
minDiff = Math.floor(minDiff_/bestPeriod);
maxDiff = Math.floor(maxDiff_/worstPeriod);
return bestPeriod;
}
// At abrupt ends of voiced words, we can have pitch periods that are better
// approximated by the previous pitch period estimate. Try to detect this case.
function prevPeriodBetter(
minDiff,
maxDiff,
preferNewPeriod)
{
if(minDiff == 0 || prevPeriod == 0) {
return false;
}
if(preferNewPeriod) {
if(maxDiff > minDiff*3) {
// Got a reasonable match this period
return false;
}
if(minDiff*2 <= prevMinDiff*3) {
// Mismatch is not that much greater this period
return false;
}
} else {
if(minDiff <= prevMinDiff) {
return false;
}
}
return true;
}
// Find the pitch period. This is a critical step, and we may have to try
// multiple ways to get a good answer. This version uses AMDF. To improve
// speed, we down sample by an integer factor get in the 11KHz range, and then
// do it again with a narrower frequency range without down sampling
function findPitchPeriod(
samples,
position,
preferNewPeriod)
{
var period, retPeriod;
var skip = 1;
if(sampleRate > SONIC_AMDF_FREQ && quality == 0) {
skip = Math.floor(sampleRate/SONIC_AMDF_FREQ);
}
if(numChannels == 1 && skip == 1) {
period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod);
} else {
downSampleInput(samples, position, skip);
period = findPitchPeriodInRange(downSampleBuffer, 0, Math.floor(minPeriod/skip),
Math.floor(maxPeriod/skip));
if(skip != 1) {
period *= skip;
var minP = period - (skip << 2);
var maxP = period + (skip << 2);
if(minP < minPeriod) {
minP = minPeriod;
}
if(maxP > maxPeriod) {
maxP = maxPeriod;
}
if(numChannels == 1) {
period = findPitchPeriodInRange(samples, position, minP, maxP);
} else {
downSampleInput(samples, position, 1);
period = findPitchPeriodInRange(downSampleBuffer, 0, minP, maxP);
}
}
}
if(prevPeriodBetter(minDiff, maxDiff, preferNewPeriod)) {
retPeriod = prevPeriod;
} else {
retPeriod = period;
}
prevMinDiff = minDiff;
prevPeriod = period;
return retPeriod;
}
// Overlap two sound segments, ramp the volume of one down, while ramping the
// other one from zero up, and add them, storing the result at the output.
function overlapAdd(
numSamples,
numChannels,
out,
outPos,
rampDown,
rampDownPos,
rampUp,
rampUpPos)
{
for(var i = 0; i < numChannels; i++) {
var o = outPos*numChannels + i;
var u = rampUpPos*numChannels + i;
var d = rampDownPos*numChannels + i;
for(var t = 0; t < numSamples; t++) {
out[o] = Math.floor((rampDown[d]*(numSamples - t) + rampUp[u]*t)/numSamples);
o += numChannels;
d += numChannels;
u += numChannels;
}
}
}
// Overlap two sound segments, ramp the volume of one down, while ramping the
// other one from zero up, and add them, storing the result at the output.
function overlapAddWithSeparation(
numSamples,
numChannels,
separation,
out,
outPos,
rampDown,
rampDownPos,
rampUp,
rampUpPos)
{
for(var i = 0; i < numChannels; i++) {
var o = outPos*numChannels + i;
var u = rampUpPos*numChannels + i;
var d = rampDownPos*numChannels + i;
for(var t = 0; t < numSamples + separation; t++) {
if(t < separation) {
out[o] = Math.floor(rampDown[d]*(numSamples - t)/numSamples);
d += numChannels;
} else if(t < numSamples) {
out[o] = Math.floor((rampDown[d]*(numSamples - t) + rampUp[u]*(t - separation))/numSamples);
d += numChannels;
u += numChannels;
} else {
out[o] = Math.floor(rampUp[u]*(t - separation)/numSamples);
u += numChannels;
}
o += numChannels;
}
}
}
// Just move the new samples in the output buffer to the pitch buffer
function moveNewSamplesToPitchBuffer(
originalNumOutputSamples)
{
var numSamples = numOutputSamples - originalNumOutputSamples;
if(numPitchSamples + numSamples > pitchBufferSize) {
pitchBufferSize += (pitchBufferSize >> 1) + numSamples;
pitchBuffer = resize(pitchBuffer, pitchBufferSize);
}
move(pitchBuffer, numPitchSamples, outputBuffer, originalNumOutputSamples, numSamples);
numOutputSamples = originalNumOutputSamples;
numPitchSamples += numSamples;
}
// Remove processed samples from the pitch buffer.
function removePitchSamples(
numSamples)
{
if(numSamples == 0) {
return;
}
move(pitchBuffer, 0, pitchBuffer, numSamples, numPitchSamples - numSamples);
numPitchSamples -= numSamples;
}
// Change the pitch. The latency this introduces could be reduced by looking at
// past samples to determine pitch, rather than future.
function adjustPitch(
originalNumOutputSamples)
{
var period, newPeriod, separation;
var position = 0;
if(numOutputSamples == originalNumOutputSamples) {
return;
}
moveNewSamplesToPitchBuffer(originalNumOutputSamples);
while(numPitchSamples - position >= maxRequired) {
period = findPitchPeriod(pitchBuffer, position, false);
newPeriod = Math.floor(period/pitch);
enlargeOutputBufferIfNeeded(newPeriod);
if(pitch >= 1.0) {
overlapAdd(newPeriod, numChannels, outputBuffer, numOutputSamples, pitchBuffer,
position, pitchBuffer, position + period - newPeriod);
} else {
separation = newPeriod - period;
overlapAddWithSeparation(period, numChannels, separation, outputBuffer, numOutputSamples,
pitchBuffer, position, pitchBuffer, position);
}
numOutputSamples += newPeriod;
position += period;
}
removePitchSamples(position);
}
// Aproximate the sinc function times a Hann window from the sinc table.
function findSincCoefficient(i, ratio, width) {
var lobePoints = Math.floor((SINC_TABLE_SIZE-1)/SINC_FILTER_POINTS);
var left = Math.floor(i*lobePoints + (ratio*lobePoints)/width);
var right = left + 1;
var position = i*lobePoints*width + ratio*lobePoints - left*width;
var leftVal = sincTable[left];
var rightVal = sincTable[right];
return Math.floor(((leftVal*(width - position) + rightVal*position) << 1)/width);
}
// Return 1 if value >= 0, else -1. This represents the sign of value.
function getSign(value) {
return value >= 0? 1 : -1;
}
// Interpolate the new output sample.
function interpolate(
in_,
inPos, // Index to first sample which already includes channel offset.
oldSampleRate,
newSampleRate)
{
// Compute N-point sinc FIR-filter here. Clip rather than overflow.
var i;
var total = 0;
var position = newRatePosition*oldSampleRate;
var leftPosition = oldRatePosition*newSampleRate;
var rightPosition = (oldRatePosition + 1)*newSampleRate;
var ratio = rightPosition - position - 1;
var width = rightPosition - leftPosition;
var weight, value;
var oldSign;
var overflowCount = 0;
for (i = 0; i < SINC_FILTER_POINTS; i++) {
weight = findSincCoefficient(i, ratio, width);
/* printf("%u %f\n", i, weight); */
value = in_[inPos + i*numChannels]*weight;
oldSign = getSign(total);
total += value;
if (oldSign != getSign(total) && getSign(value) == oldSign) {
/* We must have overflowed. This can happen with a sinc filter. */
overflowCount += oldSign;
}
}
/* It is better to clip than to wrap if there was a overflow. */
if (overflowCount > 0) {
return 0x7FFF;
} else if (overflowCount < 0) {
return -0x8000;
}
return (total >> 16)&0xffff;
}
// Change the rate.
function adjustRate(
rate,
originalNumOutputSamples)
{
var newSampleRate = Math.floor(sampleRate/rate);
var oldSampleRate = sampleRate;
var position;
// Set these values to help with the integer math
while(newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) {
newSampleRate >>= 1;
oldSampleRate >>= 1;
}
if(numOutputSamples == originalNumOutputSamples) {
return;
}
moveNewSamplesToPitchBuffer(originalNumOutputSamples);
// Leave at least one pitch sample in the buffer
for(position = 0; position < numPitchSamples - 1; position++) {
while((oldRatePosition + 1)*newSampleRate > newRatePosition*oldSampleRate) {
enlargeOutputBufferIfNeeded(1);
for(var i = 0; i < numChannels; i++) {
outputBuffer[numOutputSamples*numChannels + i] = interpolate(pitchBuffer,
position*numChannels + i, oldSampleRate, newSampleRate);
}
newRatePosition++;
numOutputSamples++;
}
oldRatePosition++;
if(oldRatePosition == oldSampleRate) {
oldRatePosition = 0;
if(newRatePosition != newSampleRate) {
throw new Error("Assertion failed: newRatePosition != newSampleRate\n");
//assert false;
}
newRatePosition = 0;
}
}
removePitchSamples(position);
}
// Skip over a pitch period, and copy period/speed samples to the output
function skipPitchPeriod(
samples,
position,
speed,
period)
{
var newSamples;
if(speed >= 2.0) {
newSamples = Math.floor(period/(speed - 1.0));
} else {
newSamples = period;
remainingInputToCopy = Math.floor(period*(2.0 - speed)/(speed - 1.0));
}
enlargeOutputBufferIfNeeded(newSamples);
overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples, samples, position,
samples, position + period);
numOutputSamples += newSamples;
return newSamples;
}
// Insert a pitch period, and determine how much input to copy directly.
function insertPitchPeriod(
samples,
position,
speed,
period)
{
var newSamples;
if(speed < 0.5) {
newSamples = Math.floor(period*speed/(1.0 - speed));
} else {
newSamples = period;
remainingInputToCopy = Math.floor(period*(2.0*speed - 1.0)/(1.0 - speed));
}
enlargeOutputBufferIfNeeded(period + newSamples);
move(outputBuffer, numOutputSamples, samples, position, period);
overlapAdd(newSamples, numChannels, outputBuffer, numOutputSamples + period, samples,
position + period, samples, position);
numOutputSamples += period + newSamples;
return newSamples;
}
// Resample as many pitch periods as we have buffered on the input. Return 0 if
// we fail to resize an input or output buffer. Also scale the output by the volume.
function changeSpeed(
speed)
{
var numSamples = numInputSamples;
var position = 0, period, newSamples;
if(numInputSamples < maxRequired) {
return;
}
do {
if(remainingInputToCopy > 0) {
newSamples = copyInputToOutput(position);
position += newSamples;
} else {
period = findPitchPeriod(inputBuffer, position, true);
if(speed > 1.0) {
newSamples = skipPitchPeriod(inputBuffer, position, speed, period);
position += period + newSamples;
} else {
newSamples = insertPitchPeriod(inputBuffer, position, speed, period);
position += newSamples;
}
}
} while(position + maxRequired <= numSamples);
removeInputSamples(position);
}
// Resample as many pitch periods as we have buffered on the input. Scale the output by the volume.
function processStreamInput()
{
var originalNumOutputSamples = numOutputSamples;
var s = speed/pitch;
var r = rate;
if(!useChordPitch) {
r *= pitch;
}
if(s > 1.00001 || s < 0.99999) {
changeSpeed(s);
} else {
copyToOutput(inputBuffer, 0, numInputSamples);
numInputSamples = 0;
}
if(useChordPitch) {
if(pitch != 1.0) {
adjustPitch(originalNumOutputSamples);
}
} else if(r != 1.0) {
adjustRate(r, originalNumOutputSamples);
}
if(volume != 1.0) {
// Adjust output volume.
scaleSamples(outputBuffer, originalNumOutputSamples, numOutputSamples - originalNumOutputSamples,
volume);
}
}
// Write the data to the input stream, and process it.
function writeShortToStream(
samples)
{
addShortSamplesToInputBuffer(samples, samples?samples.length:0);
processStreamInput();
}
/**导出Sonic对象**/
FnObj.setPitch=setPitch;
FnObj.setRate=setRate;
FnObj.setSpeed=setSpeed;
FnObj.setVolume=setVolume;
FnObj.setChordPitch=setChordPitch;
FnObj.setQuality=setQuality;
return {
New:Sonic
,flushStream:flushStream
,writeShortToStream:writeShortToStream
,readShortFromStream:readShortFromStream
};
}
return new fn(SonicFunction_set);
};
Recorder.Sonic=SonicFunction;
//Worker异步化
var sonicWorker;
Recorder.BindDestroy("sonicWorker",function(){
if(sonicWorker){
Recorder.CLog("sonicWorker Destroy");
sonicWorker&&sonicWorker.terminate();
sonicWorker=null;
};
});
//开启异步如果返回null代表不支持开启成功后必须调用flush方法否则会内存泄露
var openList={id:0};
SonicFunction.Async=function(set){
if(!HasWebWorker){
Recorder.CLog($T("Ikdz::当前环境不支持Web Worker不支持调用Sonic.Async"),3);
return null;
};
var worker=sonicWorker;
try{
var onmsg=function(e){
var ed=e.data;
var cur=wk_ctxs[ed.id];
if(ed.action=="init"){
wk_ctxs[ed.id]={
sampleRate:ed.sampleRate
,sonicObj:wk_sonic({sampleRate:ed.sampleRate})
};
}else if(!cur){
return;
};
switch(ed.action){
case "flush":
var pcm=cur.sonicObj.flush();
self.postMessage({
action:ed.action
,id:ed.id
,call:ed.call
,pcm:pcm
});
cur.sonicObj=null;
delete wk_ctxs[ed.id];
break;
case "input":
var pcm=cur.sonicObj.input(ed.pcm);
self.postMessage({
action:ed.action
,id:ed.id
,call:ed.call
,pcm:pcm
});
break;
default:
if(/^set/.test(ed.action)){
cur.sonicObj[ed.action](ed.param);
};
};
};
if(!worker){
//创建一个新Worker
var jsCode=");var wk_ctxs={};self.onmessage="+onmsg;
var sonicCode=Recorder.Sonic.toString();
var url=(window.URL||webkitURL).createObjectURL(new Blob(["var wk_sonic=(",sonicCode,jsCode], {type:"text/javascript"}));
worker=new Worker(url);
setTimeout(function(){
(window.URL||webkitURL).revokeObjectURL(url);//必须要释放,不然每次调用内存都明显泄露内存
},10000);//chrome 83 file协议下如果直接释放将会使WebWorker无法启动
worker.onmessage=function(e){
var ctx=openList[e.data.id];
if(ctx){
var fn=ctx.cbs[e.data.call];
fn&&fn(e.data);
};
};
};
var ctx=new sonicWorkerCtx(worker,set);
ctx.id=++openList.id;
openList[ctx.id]=ctx;
worker.postMessage({
action:"init"
,id:ctx.id
,sampleRate:set.sampleRate
,x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
sonicWorker=worker;
return ctx;
}catch(e){//出错了就不要提供了
worker&&worker.terminate();
console.error(e);
return null;
};
};
var sonicWorkerCtx=function(worker,set){
this.worker=worker;
this.set=set;
this.cbs={i:0};
};
sonicWorkerCtx.prototype={
cb:function(call){
var This=this;
var id="cb"+(++This.cbs.i);
This.cbs[id]=function(data){
delete This.cbs[id];
call(data);
};
return id;
}
,flush:function(call){
var This=this;if(!This.worker)return;
This.worker.postMessage({
action:"flush"
,id:This.id
,call:This.cb(function(data){
call&&call(data.pcm);
This.worker=null;
delete openList[This.id];
//疑似泄露检测 排除id
var opens=-1;
for(var k in openList){
opens++;
};
if(opens){
Recorder.CLog($T("IC5Y::sonic worker剩{1}个未flush",0,opens),3);
};
})
});
}
,input:function(pcm,call){
var This=this;if(!This.worker)return;
This.worker.postMessage({
action:"input"
,id:This.id
,pcm:pcm
,call:This.cb(function(data){
call&&call(data.pcm);
})
});
}
};
var addWorkerCtxSet=function(key){
sonicWorkerCtx.prototype[key]=function(param){
var This=this;if(!This.worker)return;
This.worker.postMessage({
action:key
,id:This.id
,param:param
});
}
};
addWorkerCtxSet("setPitch");
addWorkerCtxSet("setRate");
addWorkerCtxSet("setSpeed");
addWorkerCtxSet("setVolume");
addWorkerCtxSet("setChordPitch");
addWorkerCtxSet("setQuality");
}));