peteris.rocks

Web Audio API playback rate preserve pitch

How to preserve the pitch when playing back an audio clip faster or slower than normal speed

Last updated on

I know next to nothing about audio.

But I do know that when I change the playback speed on YouTube videos to 2x or 0.5x, the voices sound fine.

When I do the same with <audio>, the voices sound distorted.

Apperantly, there's an algorithm that can fix it.

A JavaScript implementation of such an algorithm is available on GitHub named soundtouch-js but there is no documentation, it has not been updated in 4 years, and the demo does not work out of the box.

Here is how I made it work.

test.html has a link to the underscore library that no longer works. I removed everything else since I could not figure out what it does.

<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="src/js/core.js"></script>
<script src="src/js/pipe.js"></script>
<script src="src/js/rate-transposer.js"></script>
<script src="src/js/buffer.js"></script>
<script src="src/js/filter.js"></script>
<script src="src/js/stretch.js"></script>
<script src="src/js/soundtouch.js"></script>
<script src="test.js"></script>

test.js has been modified to look like this:

var t = new RateTransposer(true);
var s = new Stretch(true);
s.tempo = 0.75;
//t.rate = 1;

var context = new AudioContext();
var buffer;

function loadSample(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';
    request.onload = function() {
      context.decodeAudioData(request.response, function(data) {
        buffer = data;
        play();
      })
    }
    request.send();
}

loadSample('track.mp3')

var BUFFER_SIZE = 1024;
var samples = new Float32Array(BUFFER_SIZE * 2);
var node = context.createScriptProcessor(BUFFER_SIZE, 2, 2);

node.onaudioprocess = function (e) {
    var l = e.outputBuffer.getChannelData(0);
    var r = e.outputBuffer.getChannelData(1);
    var framesExtracted = f.extract(samples, BUFFER_SIZE);
    if (framesExtracted == 0) {
        pause();
    }
    for (var i = 0; i < framesExtracted; i++) {
        l[i] = samples[i * 2];
        r[i] = samples[i * 2 + 1];
    }
};

function play() {
    node.connect(context.destination);
}

function pause() {
    node.disconnect();
}

var source = {
    extract: function (target, numFrames, position) {
        var l = buffer.getChannelData(0);
        var r = buffer.getChannelData(1);
        for (var i = 0; i < numFrames; i++) {
            target[i * 2] = l[i + position];
            target[i * 2 + 1] = r[i + position];
        }
        return Math.min(numFrames, l.length - position);
    }
};


f = new SimpleFilter(source, s);

It'll load track.mp3 with XMLHttpRequest, then call the play() function which will play the audio. You can change the playback rate with s.tempo = 0.5.