├── src └── js │ ├── buffer.js │ ├── core.js │ ├── filter.js │ ├── pipe.js │ ├── rate-transposer.js │ ├── soundtouch.js │ └── stretch.js ├── test.html └── test.js /src/js/buffer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SoundTouch JS audio processing library 3 | * Copyright (c) Olli Parviainen 4 | * Copyright (c) Ryan Berdeen 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | function FifoSampleBuffer() { 22 | this._vector = new Float32Array(); 23 | this._position = 0; 24 | this._frameCount = 0; 25 | } 26 | 27 | FifoSampleBuffer.prototype = { 28 | get vector() { 29 | return this._vector; 30 | }, 31 | 32 | get position() { 33 | return this._position; 34 | }, 35 | 36 | get startIndex() { 37 | return this._position * 2; 38 | }, 39 | 40 | get frameCount() { 41 | return this._frameCount; 42 | }, 43 | 44 | get endIndex() { 45 | return (this._position + this._frameCount) * 2; 46 | }, 47 | 48 | clear: function() { 49 | this.receive(frameCount); 50 | this.rewind(); 51 | }, 52 | 53 | put: function (numFrames) { 54 | this._frameCount += numFrames; 55 | }, 56 | 57 | putSamples: function (samples, position, numFrames) { 58 | position = position || 0; 59 | var sourceOffset = position * 2; 60 | if (!(numFrames >= 0)) { 61 | numFrames = (samples.length - sourceOffset) / 2; 62 | } 63 | var numSamples = numFrames * 2; 64 | 65 | this.ensureCapacity(numFrames + this._frameCount); 66 | 67 | var destOffset = this.endIndex; 68 | this._vector.set(samples.subarray(sourceOffset, sourceOffset + numSamples), destOffset); 69 | 70 | this._frameCount += numFrames; 71 | }, 72 | 73 | putBuffer: function (buffer, position, numFrames) { 74 | position = position || 0; 75 | if (!(numFrames >= 0)) { 76 | numFrames = buffer.frameCount - position; 77 | } 78 | this.putSamples(buffer.vector, buffer.position + position, numFrames); 79 | }, 80 | 81 | receive: function (numFrames) { 82 | if (!(numFrames >= 0) || numFrames > this._frameCount) { 83 | numFrames = this._frameCount 84 | } 85 | this._frameCount -= numFrames; 86 | this._position += numFrames; 87 | }, 88 | 89 | receiveSamples: function (output, numFrames) { 90 | var numSamples = numFrames * 2; 91 | var sourceOffset = this.startIndex; 92 | output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples)); 93 | this.receive(numFrames); 94 | }, 95 | 96 | extract: function (output, position, numFrames) { 97 | var sourceOffset = this.startIndex + position * 2; 98 | var numSamples = numFrames * 2; 99 | output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples)); 100 | }, 101 | 102 | ensureCapacity: function (numFrames) { 103 | var minLength = numFrames * 2; 104 | if (this._vector.length < minLength) { 105 | var newVector = new Float32Array(minLength); 106 | newVector.set(this._vector.subarray(this.startIndex, this.endIndex)); 107 | this._vector = newVector; 108 | this._position = 0; 109 | } 110 | else { 111 | this.rewind(); 112 | } 113 | }, 114 | 115 | ensureAdditionalCapacity: function (numFrames) { 116 | this.ensureCapacity(this.frameCount + numFrames); 117 | }, 118 | 119 | rewind: function () { 120 | if (this._position > 0) { 121 | this._vector.set(this._vector.subarray(this.startIndex, this.endIndex)); 122 | this._position = 0; 123 | } 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /src/js/core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SoundTouch JS audio processing library 3 | * Copyright (c) Olli Parviainen 4 | * Copyright (c) Ryan Berdeen 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | function extend(a,b) { 22 | for ( var i in b ) { 23 | var g = b.__lookupGetter__(i), s = b.__lookupSetter__(i); 24 | 25 | if ( g || s ) { 26 | if ( g ) 27 | a.__defineGetter__(i, g); 28 | if ( s ) 29 | a.__defineSetter__(i, s); 30 | } else 31 | a[i] = b[i]; 32 | } 33 | return a; 34 | } 35 | 36 | function testFloatEqual(a, b) { 37 | return (a > b ? a - b : b - a) > 1e-10; 38 | } 39 | -------------------------------------------------------------------------------- /src/js/filter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SoundTouch JS audio processing library 3 | * Copyright (c) Olli Parviainen 4 | * Copyright (c) Ryan Berdeen 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | function FilterSupport(pipe) { 22 | this._pipe = pipe; 23 | } 24 | 25 | FilterSupport.prototype = { 26 | get pipe() { 27 | return this._pipe; 28 | }, 29 | 30 | get inputBuffer() { 31 | return this._pipe.inputBuffer; 32 | }, 33 | 34 | get outputBuffer() { 35 | return this._pipe.outputBuffer; 36 | }, 37 | 38 | // fillInputBuffer: function(numFrames) { 39 | // throw new Error("fillInputBuffer() not overridden"); 40 | // }, 41 | 42 | fillOutputBuffer: function(numFrames) { 43 | while (this.outputBuffer.frameCount < numFrames) { 44 | // TODO hardcoded buffer size 45 | var numInputFrames = (8192 * 2) - this.inputBuffer.frameCount; 46 | 47 | this.fillInputBuffer(numInputFrames); 48 | 49 | if (this.inputBuffer.frameCount < (8192 * 2)) { 50 | break; 51 | // TODO flush pipe 52 | } 53 | this._pipe.process(); 54 | } 55 | }, 56 | 57 | clear: function() { 58 | this._pipe.clear(); 59 | } 60 | }; 61 | 62 | function SimpleFilter(sourceSound, pipe) { 63 | FilterSupport.call(this, pipe); 64 | this.sourceSound = sourceSound; 65 | this.historyBufferSize = 22050; 66 | this._sourcePosition = 0; 67 | this.outputBufferPosition = 0; 68 | this._position = 0; 69 | } 70 | 71 | extend(SimpleFilter.prototype, FilterSupport.prototype); 72 | 73 | extend(SimpleFilter.prototype, { 74 | get position() { 75 | return this._position; 76 | }, 77 | 78 | set position(position) { 79 | if (position > this._position) { 80 | throw new RangeError('New position may not be greater than current position'); 81 | } 82 | var newOutputBufferPosition = this.outputBufferPosition - (this._position - position); 83 | if (newOutputBufferPosition < 0) { 84 | throw new RangeError('New position falls outside of history buffer'); 85 | } 86 | this.outputBufferPosition = newOutputBufferPosition; 87 | this._position = position; 88 | }, 89 | 90 | get sourcePosition() { 91 | return this._sourcePosition; 92 | }, 93 | 94 | set sourcePosition(sourcePosition) { 95 | this.clear(); 96 | this._sourcePosition = sourcePosition; 97 | }, 98 | 99 | fillInputBuffer: function(numFrames) { 100 | var samples = new Float32Array(numFrames * 2); 101 | var numFramesExtracted = this.sourceSound.extract(samples, numFrames, this._sourcePosition); 102 | this._sourcePosition += numFramesExtracted; 103 | this.inputBuffer.putSamples(samples, 0, numFramesExtracted); 104 | }, 105 | 106 | extract: function(target, numFrames) { 107 | this.fillOutputBuffer(this.outputBufferPosition + numFrames); 108 | 109 | var numFramesExtracted = Math.min(numFrames, this.outputBuffer.frameCount - this.outputBufferPosition); 110 | this.outputBuffer.extract(target, this.outputBufferPosition, numFramesExtracted); 111 | 112 | var currentFrames = this.outputBufferPosition + numFramesExtracted; 113 | this.outputBufferPosition = Math.min(this.historyBufferSize, currentFrames); 114 | this.outputBuffer.receive(Math.max(currentFrames - this.historyBufferSize, 0)); 115 | 116 | this._position += numFramesExtracted; 117 | return numFramesExtracted; 118 | }, 119 | 120 | handleSampleData: function(e) { 121 | this.extract(e.data, 4096); 122 | }, 123 | 124 | clear: function() { 125 | // TODO yuck 126 | FilterSupport.prototype.clear.call(this); 127 | this.outputBufferPosition = 0; 128 | } 129 | }); 130 | -------------------------------------------------------------------------------- /src/js/pipe.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SoundTouch JS audio processing library 3 | * Copyright (c) Olli Parviainen 4 | * Copyright (c) Ryan Berdeen 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | 22 | function AbstractFifoSamplePipe(createBuffers) { 23 | if (createBuffers) { 24 | this.inputBuffer = new FifoSampleBuffer(); 25 | this.outputBuffer = new FifoSampleBuffer(); 26 | } 27 | else { 28 | this.inputBuffer = this.outputBuffer = null; 29 | } 30 | } 31 | 32 | AbstractFifoSamplePipe.prototype = { 33 | get inputBuffer() { 34 | return this._inputBuffer; 35 | }, 36 | 37 | set inputBuffer (inputBuffer) { 38 | this._inputBuffer = inputBuffer; 39 | }, 40 | 41 | get outputBuffer() { 42 | return this._outputBuffer; 43 | }, 44 | 45 | set outputBuffer(outputBuffer) { 46 | this._outputBuffer = outputBuffer; 47 | }, 48 | 49 | clear: function () { 50 | this._inputBuffer.clear(); 51 | this._outputBuffer.clear(); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/js/rate-transposer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SoundTouch JS audio processing library 3 | * Copyright (c) Olli Parviainen 4 | * Copyright (c) Ryan Berdeen 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | function RateTransposer(createBuffers) { 22 | AbstractFifoSamplePipe.call(this, createBuffers); 23 | this._reset(); 24 | this.rate = 1; 25 | } 26 | 27 | extend(RateTransposer.prototype, AbstractFifoSamplePipe.prototype); 28 | extend(RateTransposer.prototype, { 29 | set rate(rate) { 30 | this._rate = rate; 31 | // TODO aa filter 32 | }, 33 | 34 | _reset: function () { 35 | this.slopeCount = 0; 36 | this.prevSampleL = 0; 37 | this.prevSampleR = 0; 38 | }, 39 | 40 | clone: function () { 41 | var result = new RateTransposer(); 42 | result.rate = this._rate; 43 | return result; 44 | }, 45 | 46 | process: function () { 47 | // TODO aa filter 48 | var numFrames = this._inputBuffer.frameCount; 49 | this._outputBuffer.ensureAdditionalCapacity(numFrames / this._rate + 1); 50 | var numFramesOutput = this._transpose(numFrames); 51 | this._inputBuffer.receive(); 52 | this._outputBuffer.put(numFramesOutput); 53 | }, 54 | 55 | _transpose: function (numFrames) { 56 | if (numFrames == 0) { 57 | // no work 58 | return 0; 59 | } 60 | 61 | var src = this._inputBuffer.vector; 62 | var srcOffset = this._inputBuffer.startIndex; 63 | 64 | var dest = this._outputBuffer.vector; 65 | var destOffset = this._outputBuffer.endIndex; 66 | 67 | var used = 0; 68 | var i = 0; 69 | 70 | while(this.slopeCount < 1.0) { 71 | dest[destOffset + 2 * i] = (1.0 - this.slopeCount) * this.prevSampleL + this.slopeCount * src[srcOffset]; 72 | dest[destOffset + 2 * i + 1] = (1.0 - this.slopeCount) * this.prevSampleR + this.slopeCount * src[srcOffset + 1]; 73 | i++; 74 | this.slopeCount += this._rate; 75 | } 76 | 77 | this.slopeCount -= 1.0; 78 | 79 | if (numFrames != 1) { 80 | out: while (true) { 81 | while (this.slopeCount > 1.0) { 82 | this.slopeCount -= 1.0; 83 | used++; 84 | if (used >= numFrames - 1) { 85 | break out; 86 | } 87 | } 88 | 89 | var srcIndex = srcOffset + 2 * used; 90 | dest[destOffset + 2 * i] = (1.0 - this.slopeCount) * src[srcIndex] + this.slopeCount * src[srcIndex + 2]; 91 | dest[destOffset + 2 * i + 1] = (1.0 - this.slopeCount) * src[srcIndex + 1] + this.slopeCount * src[srcIndex + 3]; 92 | 93 | i++; 94 | this.slopeCount += this._rate; 95 | } 96 | } 97 | 98 | this.prevSampleL = src[srcOffset + 2 * numFrames - 2]; 99 | this.prevSampleR = src[srcOffset + 2 * numFrames - 1]; 100 | 101 | return i; 102 | } 103 | }); -------------------------------------------------------------------------------- /src/js/soundtouch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SoundTouch JS audio processing library 3 | * Copyright (c) Olli Parviainen 4 | * Copyright (c) Ryan Berdeen 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | function SoundTouch() { 22 | this.rateTransposer = new RateTransposer(false); 23 | this.tdStretch = new Stretch(false); 24 | 25 | this._inputBuffer = new FifoSampleBuffer(); 26 | this._intermediateBuffer = new FifoSampleBuffer(); 27 | this._outputBuffer = new FifoSampleBuffer(); 28 | 29 | this._rate = 0; 30 | this.tempo = 0; 31 | 32 | this.virtualPitch = 1.0; 33 | this.virtualRate = 1.0; 34 | this.virtualTempo = 1.0; 35 | 36 | this._calculateEffectiveRateAndTempo(); 37 | } 38 | 39 | extend(SoundTouch.prototype, { 40 | clear: function () { 41 | rateTransposer.clear(); 42 | tdStretch.clear(); 43 | }, 44 | 45 | clone: function () { 46 | var result = new SoundTouch(); 47 | result.rate = rate; 48 | result.tempo = tempo; 49 | return result; 50 | }, 51 | 52 | get rate() { 53 | return this._rate; 54 | }, 55 | 56 | set rate(rate) { 57 | this.virtualRate = rate; 58 | this._calculateEffectiveRateAndTempo(); 59 | }, 60 | 61 | set rateChange(rateChange) { 62 | this.rate = 1.0 + 0.01 * rateChange; 63 | }, 64 | 65 | get tempo() { 66 | return this._tempo; 67 | }, 68 | 69 | set tempo(tempo) { 70 | this.virtualTempo = tempo; 71 | this._calculateEffectiveRateAndTempo(); 72 | }, 73 | 74 | set tempoChange(tempoChange) { 75 | this.tempo = 1.0 + 0.01 * tempoChange; 76 | }, 77 | 78 | set pitch(pitch) { 79 | this.virtualPitch = pitch; 80 | this._calculateEffectiveRateAndTempo(); 81 | }, 82 | 83 | set pitchOctaves(pitchOctaves) { 84 | this.pitch = Math.exp(0.69314718056 * pitchOctaves); 85 | this._calculateEffectiveRateAndTempo(); 86 | }, 87 | 88 | set pitchSemitones(pitchSemitones) { 89 | this.pitchOctaves = pitchSemitones / 12.0; 90 | }, 91 | 92 | get inputBuffer() { 93 | return this._inputBuffer; 94 | }, 95 | 96 | get outputBuffer() { 97 | return this._outputBuffer; 98 | }, 99 | 100 | _calculateEffectiveRateAndTempo: function () { 101 | var previousTempo = this._tempo; 102 | var previousRate = this._rate; 103 | 104 | this._tempo = this.virtualTempo / this.virtualPitch; 105 | this._rate = this.virtualRate * this.virtualPitch; 106 | 107 | if (testFloatEqual(this._tempo, previousTempo)) { 108 | this.tdStretch.tempo = this._tempo; 109 | } 110 | if (testFloatEqual(this._rate, previousRate)) { 111 | this.rateTransposer.rate = this._rate; 112 | } 113 | 114 | if (this._rate > 1.0) { 115 | if (this._outputBuffer != this.rateTransposer.outputBuffer) { 116 | this.tdStretch.inputBuffer = this._inputBuffer; 117 | this.tdStretch.outputBuffer = this._intermediateBuffer; 118 | 119 | this.rateTransposer.inputBuffer = this._intermediateBuffer; 120 | this.rateTransposer.outputBuffer = this._outputBuffer; 121 | } 122 | } 123 | else { 124 | if (this._outputBuffer != this.tdStretch.outputBuffer) { 125 | this.rateTransposer.inputBuffer = this._inputBuffer; 126 | this.rateTransposer.outputBuffer = this._intermediateBuffer; 127 | 128 | this.tdStretch.inputBuffer = this._intermediateBuffer; 129 | this.tdStretch.outputBuffer = this._outputBuffer; 130 | } 131 | } 132 | }, 133 | 134 | process: function () { 135 | if (this._rate > 1.0) { 136 | this.tdStretch.process(); 137 | this.rateTransposer.process(); 138 | } 139 | else { 140 | this.rateTransposer.process(); 141 | this.tdStretch.process(); 142 | } 143 | } 144 | }); 145 | -------------------------------------------------------------------------------- /src/js/stretch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SoundTouch JS audio processing library 3 | * Copyright (c) Olli Parviainen 4 | * Copyright (c) Ryan Berdeen 5 | * 6 | * This library is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 2.1 of the License, or (at your option) any later version. 10 | * 11 | * This library is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this library; if not, write to the Free Software 18 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | */ 20 | 21 | //'use strict'; 22 | 23 | /** 24 | * Giving this value for the sequence length sets automatic parameter value 25 | * according to tempo setting (recommended) 26 | */ 27 | var USE_AUTO_SEQUENCE_LEN = 0; 28 | 29 | /** 30 | * Default length of a single processing sequence, in milliseconds. This determines to how 31 | * long sequences the original sound is chopped in the time-stretch algorithm. 32 | * 33 | * The larger this value is, the lesser sequences are used in processing. In principle 34 | * a bigger value sounds better when slowing down tempo, but worse when increasing tempo 35 | * and vice versa. 36 | * 37 | * Increasing this value reduces computational burden and vice versa. 38 | */ 39 | //var DEFAULT_SEQUENCE_MS = 130 40 | var DEFAULT_SEQUENCE_MS = USE_AUTO_SEQUENCE_LEN; 41 | 42 | /** 43 | * Giving this value for the seek window length sets automatic parameter value 44 | * according to tempo setting (recommended) 45 | */ 46 | var USE_AUTO_SEEKWINDOW_LEN = 0; 47 | 48 | /** 49 | * Seeking window default length in milliseconds for algorithm that finds the best possible 50 | * overlapping location. This determines from how wide window the algorithm may look for an 51 | * optimal joining location when mixing the sound sequences back together. 52 | * 53 | * The bigger this window setting is, the higher the possibility to find a better mixing 54 | * position will become, but at the same time large values may cause a "drifting" artifact 55 | * because consequent sequences will be taken at more uneven intervals. 56 | * 57 | * If there's a disturbing artifact that sounds as if a constant frequency was drifting 58 | * around, try reducing this setting. 59 | * 60 | * Increasing this value increases computational burden and vice versa. 61 | */ 62 | //var DEFAULT_SEEKWINDOW_MS = 25; 63 | var DEFAULT_SEEKWINDOW_MS = USE_AUTO_SEEKWINDOW_LEN; 64 | 65 | /** 66 | * Overlap length in milliseconds. When the chopped sound sequences are mixed back together, 67 | * to form a continuous sound stream, this parameter defines over how long period the two 68 | * consecutive sequences are let to overlap each other. 69 | * 70 | * This shouldn't be that critical parameter. If you reduce the DEFAULT_SEQUENCE_MS setting 71 | * by a large amount, you might wish to try a smaller value on this. 72 | * 73 | * Increasing this value increases computational burden and vice versa. 74 | */ 75 | var DEFAULT_OVERLAP_MS = 8; 76 | 77 | // Table for the hierarchical mixing position seeking algorithm 78 | var _SCAN_OFFSETS = [ 79 | [ 124, 186, 248, 310, 372, 434, 496, 558, 620, 682, 744, 806, 80 | 868, 930, 992, 1054, 1116, 1178, 1240, 1302, 1364, 1426, 1488, 0], 81 | [-100, -75, -50, -25, 25, 50, 75, 100, 0, 0, 0, 0, 82 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 83 | [ -20, -15, -10, -5, 5, 10, 15, 20, 0, 0, 0, 0, 84 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 85 | [ -4, -3, -2, -1, 1, 2, 3, 4, 0, 0, 0, 0, 86 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]; 87 | 88 | // Adjust tempo param according to tempo, so that variating processing sequence length is used 89 | // at varius tempo settings, between the given low...top limits 90 | var AUTOSEQ_TEMPO_LOW = 0.5; // auto setting low tempo range (-50%) 91 | var AUTOSEQ_TEMPO_TOP = 2.0; // auto setting top tempo range (+100%) 92 | 93 | // sequence-ms setting values at above low & top tempo 94 | var AUTOSEQ_AT_MIN = 125.0; 95 | var AUTOSEQ_AT_MAX = 50.0; 96 | var AUTOSEQ_K = ((AUTOSEQ_AT_MAX - AUTOSEQ_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW)); 97 | var AUTOSEQ_C = (AUTOSEQ_AT_MIN - (AUTOSEQ_K) * (AUTOSEQ_TEMPO_LOW)); 98 | 99 | // seek-window-ms setting values at above low & top tempo 100 | var AUTOSEEK_AT_MIN = 25.0; 101 | var AUTOSEEK_AT_MAX = 15.0; 102 | var AUTOSEEK_K = ((AUTOSEEK_AT_MAX - AUTOSEEK_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW)); 103 | var AUTOSEEK_C = (AUTOSEEK_AT_MIN - (AUTOSEEK_K) * (AUTOSEQ_TEMPO_LOW)); 104 | 105 | function Stretch(createBuffers) { 106 | AbstractFifoSamplePipe.call(this, createBuffers); 107 | this.bQuickSeek = true; 108 | this.bMidBufferDirty = false; 109 | 110 | this.pMidBuffer = null; 111 | this.overlapLength = 0; 112 | 113 | this.bAutoSeqSetting = true; 114 | this.bAutoSeekSetting = true; 115 | 116 | this._tempo = 1; 117 | this.setParameters(44100, DEFAULT_SEQUENCE_MS, DEFAULT_SEEKWINDOW_MS, DEFAULT_OVERLAP_MS); 118 | } 119 | 120 | extend(Stretch.prototype, AbstractFifoSamplePipe.prototype); 121 | 122 | extend(Stretch.prototype, { 123 | clear: function () { 124 | AbstractFifoSamplePipe.prototype.clear.call(this); 125 | this._clearMidBuffer(); 126 | }, 127 | 128 | _clearMidBuffer: function () { 129 | if (this.bMidBufferDirty) { 130 | this.bMidBufferDirty = false; 131 | this.pMidBuffer = null; 132 | } 133 | }, 134 | 135 | /** 136 | * Sets routine control parameters. These control are certain time constants 137 | * defining how the sound is stretched to the desired duration. 138 | * 139 | * 'sampleRate' = sample rate of the sound 140 | * 'sequenceMS' = one processing sequence length in milliseconds (default = 82 ms) 141 | * 'seekwindowMS' = seeking window length for scanning the best overlapping 142 | * position (default = 28 ms) 143 | * 'overlapMS' = overlapping length (default = 12 ms) 144 | */ 145 | setParameters: function(aSampleRate, aSequenceMS, aSeekWindowMS, aOverlapMS) { 146 | // accept only positive parameter values - if zero or negative, use old values instead 147 | if (aSampleRate > 0) { 148 | this.sampleRate = aSampleRate; 149 | } 150 | if (aOverlapMS > 0) { 151 | this.overlapMs = aOverlapMS; 152 | } 153 | 154 | if (aSequenceMS > 0) { 155 | this.sequenceMs = aSequenceMS; 156 | this.bAutoSeqSetting = false; 157 | } else { 158 | // zero or below, use automatic setting 159 | this.bAutoSeqSetting = true; 160 | } 161 | 162 | if (aSeekWindowMS > 0) { 163 | this.seekWindowMs = aSeekWindowMS; 164 | this.bAutoSeekSetting = false; 165 | } else { 166 | // zero or below, use automatic setting 167 | this.bAutoSeekSetting = true; 168 | } 169 | 170 | this.calcSeqParameters(); 171 | 172 | this.calculateOverlapLength(this.overlapMs); 173 | 174 | // set tempo to recalculate 'sampleReq' 175 | this.tempo = this._tempo; 176 | }, 177 | 178 | /** 179 | * Sets new target tempo. Normal tempo = 'SCALE', smaller values represent slower 180 | * tempo, larger faster tempo. 181 | */ 182 | set tempo(newTempo) { 183 | var intskip; 184 | 185 | this._tempo = newTempo; 186 | 187 | // Calculate new sequence duration 188 | this.calcSeqParameters(); 189 | 190 | // Calculate ideal skip length (according to tempo value) 191 | this.nominalSkip = this._tempo * (this.seekWindowLength - this.overlapLength); 192 | this.skipFract = 0; 193 | intskip = Math.floor(this.nominalSkip + 0.5); 194 | 195 | // Calculate how many samples are needed in the 'inputBuffer' to 196 | // process another batch of samples 197 | this.sampleReq = Math.max(intskip + this.overlapLength, this.seekWindowLength) + this.seekLength; 198 | }, 199 | 200 | 201 | // get tempo() { 202 | // return this._tempo; 203 | // }, 204 | 205 | get inputChunkSize() { 206 | return this.sampleReq; 207 | }, 208 | 209 | get outputChunkSize() { 210 | return this.overlapLength + Math.max(0, this.seekWindowLength - 2 * this.overlapLength); 211 | }, 212 | 213 | /** 214 | * Calculates overlapInMsec period length in samples. 215 | */ 216 | calculateOverlapLength: function (overlapInMsec) { 217 | var newOvl; 218 | 219 | // TODO assert(overlapInMsec >= 0); 220 | newOvl = (this.sampleRate * overlapInMsec) / 1000; 221 | if (newOvl < 16) newOvl = 16; 222 | 223 | // must be divisible by 8 224 | newOvl -= newOvl % 8; 225 | 226 | this.overlapLength = newOvl; 227 | 228 | this.pRefMidBuffer = new Float32Array(this.overlapLength * 2); 229 | this.pMidBuffer = new Float32Array(this.overlapLength * 2); 230 | }, 231 | 232 | checkLimits: function (x, mi, ma) { 233 | return (x < mi) ? mi : ((x > ma) ? ma : x); 234 | }, 235 | 236 | /** 237 | * Calculates processing sequence length according to tempo setting 238 | */ 239 | calcSeqParameters: function() { 240 | var seq; 241 | var seek; 242 | 243 | if (this.bAutoSeqSetting) { 244 | seq = AUTOSEQ_C + AUTOSEQ_K * this._tempo; 245 | seq = this.checkLimits(seq, AUTOSEQ_AT_MAX, AUTOSEQ_AT_MIN); 246 | this.sequenceMs = Math.floor(seq + 0.5); 247 | } 248 | 249 | if (this.bAutoSeekSetting) { 250 | seek = AUTOSEEK_C + AUTOSEEK_K * this._tempo; 251 | seek = this.checkLimits(seek, AUTOSEEK_AT_MAX, AUTOSEEK_AT_MIN); 252 | this.seekWindowMs = Math.floor(seek + 0.5); 253 | } 254 | 255 | // Update seek window lengths 256 | this.seekWindowLength = Math.floor((this.sampleRate * this.sequenceMs) / 1000); 257 | this.seekLength = Math.floor((this.sampleRate * this.seekWindowMs) / 1000); 258 | }, 259 | 260 | 261 | /** 262 | * Enables/disables the quick position seeking algorithm. 263 | */ 264 | set quickSeek(enable) { 265 | this.bQuickSeek = enable; 266 | }, 267 | 268 | clone: function () { 269 | var result = new Stretch(); 270 | result.tempo = this.tempo; 271 | result.setParameters(this.sampleRate, this.sequenceMs, this.seekWindowMs, this.overlapMs); 272 | return result; 273 | }, 274 | 275 | /** 276 | * Seeks for the optimal overlap-mixing position. 277 | */ 278 | seekBestOverlapPosition: function () { 279 | if (this.bQuickSeek) { 280 | return this.seekBestOverlapPositionStereoQuick(); 281 | } 282 | else { 283 | return this.seekBestOverlapPositionStereo(); 284 | } 285 | }, 286 | 287 | /** 288 | * Seeks for the optimal overlap-mixing position. The 'stereo' version of the 289 | * routine 290 | * 291 | * The best position is determined as the position where the two overlapped 292 | * sample sequences are 'most alike', in terms of the highest cross-correlation 293 | * value over the overlapping period 294 | */ 295 | seekBestOverlapPositionStereo: function () { 296 | var bestOffs; 297 | var bestCorr 298 | var corr; 299 | var i; 300 | 301 | // Slopes the amplitudes of the 'midBuffer' samples 302 | this.precalcCorrReferenceStereo(); 303 | 304 | bestCorr = Number.MIN_VALUE; 305 | bestOffs = 0; 306 | 307 | // Scans for the best correlation value by testing each possible position 308 | // over the permitted range. 309 | for (i = 0; i < this.seekLength; i ++) { 310 | // Calculates correlation value for the mixing position corresponding 311 | // to 'i' 312 | corr = this.calcCrossCorrStereo(2 * i, this.pRefMidBuffer); 313 | 314 | // Checks for the highest correlation value 315 | if (corr > bestCorr) { 316 | bestCorr = corr; 317 | bestOffs = i; 318 | } 319 | } 320 | 321 | return bestOffs; 322 | }, 323 | 324 | /** 325 | * Seeks for the optimal overlap-mixing position. The 'stereo' version of the 326 | * routine 327 | * 328 | * The best position is determined as the position where the two overlapped 329 | * sample sequences are 'most alike', in terms of the highest cross-correlation 330 | * value over the overlapping period 331 | */ 332 | seekBestOverlapPositionStereoQuick: function () { 333 | var j; 334 | var bestOffs; 335 | var bestCorr; 336 | var corr; 337 | var scanCount; 338 | var corrOffset; 339 | var tempOffset; 340 | 341 | // Slopes the amplitude of the 'midBuffer' samples 342 | this.precalcCorrReferenceStereo(); 343 | 344 | bestCorr = Number.MIN_VALUE; 345 | bestOffs = 0; 346 | corrOffset = 0; 347 | tempOffset = 0; 348 | 349 | // Scans for the best correlation value using four-pass hierarchical search. 350 | // 351 | // The look-up table 'scans' has hierarchical position adjusting steps. 352 | // In first pass the routine searhes for the highest correlation with 353 | // relatively coarse steps, then rescans the neighbourhood of the highest 354 | // correlation with better resolution and so on. 355 | for (scanCount = 0; scanCount < 4; scanCount ++) { 356 | j = 0; 357 | while (_SCAN_OFFSETS[scanCount][j]) { 358 | tempOffset = corrOffset + _SCAN_OFFSETS[scanCount][j]; 359 | if (tempOffset >= this.seekLength) break; 360 | 361 | // Calculates correlation value for the mixing position corresponding 362 | // to 'tempOffset' 363 | corr = this.calcCrossCorrStereo(2 * tempOffset, this.pRefMidBuffer); 364 | 365 | // Checks for the highest correlation value 366 | if (corr > bestCorr) { 367 | bestCorr = corr; 368 | bestOffs = tempOffset; 369 | } 370 | j++; 371 | } 372 | corrOffset = bestOffs; 373 | } 374 | 375 | return bestOffs; 376 | }, 377 | 378 | /** 379 | * Slopes the amplitude of the 'midBuffer' samples so that cross correlation 380 | * is faster to calculate 381 | */ 382 | precalcCorrReferenceStereo: function() { 383 | var i; 384 | var cnt2; 385 | var temp; 386 | 387 | for (i = 0; i < this.overlapLength; i ++) { 388 | temp = i * (this.overlapLength - i); 389 | cnt2 = i * 2; 390 | this.pRefMidBuffer[cnt2] = this.pMidBuffer[cnt2] * temp; 391 | this.pRefMidBuffer[cnt2 + 1] = this.pMidBuffer[cnt2 + 1] * temp; 392 | } 393 | }, 394 | 395 | calcCrossCorrStereo: function(mixingPos, compare) { 396 | var mixing = this._inputBuffer.vector; 397 | mixingPos += this._inputBuffer.startIndex; 398 | 399 | var corr; 400 | var i; 401 | var mixingOffset; 402 | 403 | corr = 0; 404 | for (i = 2; i < 2 * this.overlapLength; i += 2) { 405 | mixingOffset = i + mixingPos; 406 | corr += mixing[mixingOffset] * compare[i] + 407 | mixing[mixingOffset + 1] * compare[i + 1]; 408 | } 409 | 410 | return corr; 411 | }, 412 | 413 | // TODO inline 414 | /** 415 | * Overlaps samples in 'midBuffer' with the samples in 'pInputBuffer' at position 416 | * of 'ovlPos'. 417 | */ 418 | overlap: function (ovlPos) { 419 | this.overlapStereo(2 * ovlPos); 420 | }, 421 | 422 | /** 423 | * Overlaps samples in 'midBuffer' with the samples in 'pInput' 424 | */ 425 | overlapStereo: function(pInputPos) { 426 | var pInput = this._inputBuffer.vector; 427 | pInputPos += this._inputBuffer.startIndex; 428 | 429 | var pOutput = this._outputBuffer.vector; 430 | var pOutputPos = this._outputBuffer.endIndex; 431 | 432 | var i; 433 | var cnt2; 434 | var fTemp; 435 | var fScale; 436 | var fi; 437 | var pInputOffset; 438 | var pOutputOffset; 439 | 440 | fScale = 1 / this.overlapLength; 441 | 442 | for (i = 0; i < this.overlapLength; i++) { 443 | fTemp = (this.overlapLength - i) * fScale; 444 | fi = i * fScale; 445 | cnt2 = 2 * i; 446 | pInputOffset = cnt2 + pInputPos; 447 | pOutputOffset = cnt2 + pOutputPos; 448 | pOutput[pOutputOffset + 0] = pInput[pInputOffset + 0] * fi + this.pMidBuffer[cnt2 + 0] * fTemp; 449 | pOutput[pOutputOffset + 1] = pInput[pInputOffset + 1] * fi + this.pMidBuffer[cnt2 + 1] * fTemp; 450 | } 451 | }, 452 | 453 | process: function() { 454 | var ovlSkip; 455 | var offset; 456 | var temp; 457 | var i; 458 | 459 | if (this.pMidBuffer == null) { 460 | // if midBuffer is empty, move the first samples of the input stream 461 | // into it 462 | if (this._inputBuffer.frameCount < this.overlapLength) { 463 | // wait until we've got overlapLength samples 464 | return; 465 | } 466 | this.pMidBuffer = new Float32Array(this.overlapLength * 2); 467 | this._inputBuffer.receiveSamples(this.pMidBuffer, this.overlapLength); 468 | } 469 | 470 | var output; 471 | // Process samples as long as there are enough samples in 'inputBuffer' 472 | // to form a processing frame. 473 | while (this._inputBuffer.frameCount >= this.sampleReq) { 474 | // If tempo differs from the normal ('SCALE'), scan for the best overlapping 475 | // position 476 | offset = this.seekBestOverlapPosition(); 477 | 478 | // Mix the samples in the 'inputBuffer' at position of 'offset' with the 479 | // samples in 'midBuffer' using sliding overlapping 480 | // ... first partially overlap with the end of the previous sequence 481 | // (that's in 'midBuffer') 482 | this._outputBuffer.ensureAdditionalCapacity(this.overlapLength); 483 | // FIXME unit? 484 | //overlap(uint(offset)); 485 | this.overlap(Math.floor(offset)); 486 | this._outputBuffer.put(this.overlapLength); 487 | 488 | // ... then copy sequence samples from 'inputBuffer' to output 489 | temp = (this.seekWindowLength - 2 * this.overlapLength); // & 0xfffffffe; 490 | if (temp > 0) { 491 | this._outputBuffer.putBuffer(this._inputBuffer, offset + this.overlapLength, temp); 492 | } 493 | 494 | // Copies the end of the current sequence from 'inputBuffer' to 495 | // 'midBuffer' for being mixed with the beginning of the next 496 | // processing sequence and so on 497 | //assert(offset + seekWindowLength <= (int)inputBuffer.numSamples()); 498 | var start = this.inputBuffer.startIndex + 2 * (offset + this.seekWindowLength - this.overlapLength); 499 | this.pMidBuffer.set(this._inputBuffer.vector.subarray(start, start + 2 * this.overlapLength)) 500 | 501 | // Remove the processed samples from the input buffer. Update 502 | // the difference between integer & nominal skip step to 'skipFract' 503 | // in order to prevent the error from accumulating over time. 504 | this.skipFract += this.nominalSkip; // real skip size 505 | ovlSkip = Math.floor(this.skipFract); // rounded to integer skip 506 | this.skipFract -= ovlSkip; // maintain the fraction part, i.e. real vs. integer skip 507 | this._inputBuffer.receive(ovlSkip); 508 | } 509 | } 510 | }); 511 | 512 | // https://bugs.webkit.org/show_bug.cgi?id=57295 513 | extend(Stretch.prototype, { 514 | get tempo() { 515 | return this._tempo; 516 | } 517 | }); 518 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var t = new RateTransposer(true); 2 | var s = new Stretch(true); 3 | //s.tempo = .5; 4 | t.rate = 2; 5 | var context = new webkitAudioContext(); 6 | 7 | var buffer; 8 | 9 | loadSample = function(url) { 10 | var request = new XMLHttpRequest(); 11 | request.open('GET', url, true); 12 | request.responseType = 'arraybuffer'; 13 | 14 | request.onload = function() { 15 | console.log('url loaded'); 16 | createBuffer(request.response); 17 | } 18 | 19 | console.log('reading url'); 20 | request.send(); 21 | } 22 | 23 | function createBuffer(arrayBuffer) { 24 | offset = 0; 25 | startTime = 0; 26 | var start = new Date(); 27 | // NOTE the second parameter is required, or a TypeError is thrown 28 | buffer = context.createBuffer(arrayBuffer, false); 29 | console.log('loaded audio in ' + (new Date() - start)); 30 | } 31 | 32 | //loadSample('badromance.mp3') 33 | loadSample('track.mp3') 34 | 35 | var BUFFER_SIZE = 1024; 36 | 37 | var node = context.createJavaScriptNode(BUFFER_SIZE, 2, 2); 38 | 39 | var samples = new Float32Array(BUFFER_SIZE * 2); 40 | 41 | node.onaudioprocess = function (e) { 42 | var l = e.outputBuffer.getChannelData(0); 43 | var r = e.outputBuffer.getChannelData(1); 44 | var framesExtracted = f.extract(samples, BUFFER_SIZE); 45 | if (framesExtracted == 0) { 46 | pause(); 47 | } 48 | for (var i = 0; i < framesExtracted; i++) { 49 | l[i] = samples[i * 2]; 50 | r[i] = samples[i * 2 + 1]; 51 | } 52 | }; 53 | 54 | function play() { 55 | node.connect(context.destination); 56 | } 57 | 58 | function pause() { 59 | node.disconnect(); 60 | } 61 | 62 | var source = { 63 | extract: function (target, numFrames, position) { 64 | var l = buffer.getChannelData(0); 65 | var r = buffer.getChannelData(1); 66 | for (var i = 0; i < numFrames; i++) { 67 | target[i * 2] = l[i + position]; 68 | target[i * 2 + 1] = r[i + position]; 69 | } 70 | return Math.min(numFrames, l.length - position); 71 | } 72 | }; 73 | 74 | 75 | f = new SimpleFilter(source, s); 76 | --------------------------------------------------------------------------------