├── 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 |
--------------------------------------------------------------------------------