19

I'm playing around with the Web Audio API & trying to find a way to import an mp3 (so therefore this is only in Chrome), and generate a waveform of it on a canvas. I can do this in real-time, but my goal is to do this faster than real-time.

All the examples I've been able to find involve reading the frequency data from an analyser object, in a function attached to the onaudioprocess event:

processor = context.createJavascriptNode(2048,1,1);
processor.onaudioprocess = processAudio;
...
function processAudio{
    var freqByteData = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(freqByteData);
    //calculate magnitude & render to canvas
}

It appears though, that analyser.frequencyBinCount is only populated when the sound is playing (something about the buffer being filled).

What I want is to be able to manually/programmatically step through the file as fast as possible, to generate the canvas image.

What I've got so far is this:

$("#files").on('change',function(e){
    var FileList = e.target.files,
        Reader = new FileReader();

    var File = FileList[0];

    Reader.onload = (function(theFile){
        return function(e){
            context.decodeAudioData(e.target.result,function(buffer){
                source.buffer = buffer;
                source.connect(analyser);
                analyser.connect(jsNode);

                var freqData = new Uint8Array(buffer.getChannelData(0));

                console.dir(analyser);
                console.dir(jsNode);

                jsNode.connect(context.destination);
                //source.noteOn(0);
            });
        };
    })(File);

    Reader.readAsArrayBuffer(File);
});

But getChannelData() always returns an empty typed array.

Any insight is appreciated - even if it turns out it can't be done. I think I'm the only one the Internet not wanting to do stuff in real-time.

Thanks.

Quasipickle
  • 4,383
  • 1
  • 31
  • 53
  • 1
    ya - faster than real-time. As in, if the duration of the track is 5 minutes, I don't want to wait 5 minutes to generate the waveform. I want to process it as fast as possible (hopefully a few seconds) – Quasipickle Nov 12 '11 at 09:30
  • 1
    @Pickle, you are filling the `Uint8Array` wrong. See a working solution [here](https://github.com/katspaugh/wavesurfer.js/blob/master/src/webaudio.js). – katspaugh Sep 25 '12 at 11:19

2 Answers2

26

There is a really amazing 'offline' mode of the Web Audio API that allows you to pre-process an entire file through an audio context and then do something with the result:

var context = new webkitOfflineAudioContext();

var source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.noteOn(0);

context.oncomplete = function(e) {
  var audioBuffer = e.renderedBuffer;
};

context.startRendering();

So the setup looks exactly the same as the real-time processing mode, except you set up the oncomplete callback and the call to startRendering(). What you get back in e.redneredBuffer is an AudioBuffer.

ruidlopes
  • 43
  • 4
ebidel
  • 23,921
  • 3
  • 63
  • 76
  • It's really late right now and I'm working on other stuff, but this answer sounds really promising (basically because it's the only one I've gotten). I'll be sure to let you know how this goes. – Quasipickle Nov 12 '11 at 09:14
  • 1
    OK, I've been playing with this for the better part of an hour and I'm lost. How would your code fit in with mine? When you `set source.buffer = buffer`, where does `buffer` come from? The only place I can see an AudioBuffer being created, is as an argument to the success function of context.decodeAudioData(). Here's a jsfiddle of what I've got. It's a complete mess, but its where I'm at - pretty much stabbing in the dark. http://jsfiddle.net/NW7E3/ Make sure to have the console active – Quasipickle Nov 12 '11 at 10:33
  • Right. `buffer` is an `AudioBuffer` that you get from `decodeAudioData` – ebidel Nov 15 '11 at 09:57
  • Add a local AudioContext in your `oncomplete `callback: `var context = new webkitAudioContext();`. Essentially, I think you need to create a context for playing and a context for processing. – ebidel Nov 15 '11 at 10:03
  • I think your example is missing something. When real-time processing, data only gets fed to the processor when the `source.noteOn(0)` gets called. What is it that triggers the processing when `startRendering()` is used? Do you have any documentation about this? I've been able to find nothing except patch notes. JSFiddle of real-time processing: http://jsfiddle.net/QpzQm/10/ JSFiddle of my best guess as to how startRendering() is used (but which actually does nothing) http://jsfiddle.net/9TEKG/ – Quasipickle Nov 16 '11 at 06:33
  • 1
    @Pickle did you ever get anywhere with this? – lakenen Mar 25 '12 at 23:33
  • No - the standard is still too fluid & the documentation to sparse. It's still being developed though. – Quasipickle Mar 26 '12 at 03:32
  • Could this by any chance work with a `mediaElementSourceNode`? http://stackoverflow.com/questions/11292076/load-audiodata-into-audiobuffersourcenode-from-audio-element-via-createmediae – gherkins Jul 14 '12 at 10:36
  • I'm working on this problem too (http://stackoverflow.com/q/13590999/1015178). The example here http://code.google.com/p/chromium/source/browse/trunk/samples/audio/offline-convolution.html?r=2997 uses a `webkitAudioContext` constructor with a few additional arguments, but this only works on Mac. Errors occur in Windows and Linux. – John Vinyard Nov 27 '12 at 19:07
6

I got this to work using OfflineAudioContext using the following code. The complete example here shows how to use it to compute the FFT magnitudes for a linear chirp. Once you have the concept of hooking the nodes together, you can do just about anything with it offline.

function fsin(freq, phase, t) {
  return Math.sin(2 * Math.PI * freq * t + phase)
}

function linearChirp(startFreq, endFreq, duration, sampleRate) {
  if (duration === undefined) {
    duration = 1; // seconds
  }
  if (sampleRate === undefined) {
    sampleRate = 44100; // per second
  }
  var numSamples = Math.floor(duration * sampleRate);
  var chirp = new Array(numSamples);
  var df = (endFreq - startFreq) / numSamples;
  for (var i = 0; i < numSamples; i++) {
    chirp[i] = fsin(startFreq + df * i, 0, i / sampleRate);
  }
  return chirp;
}

function AnalyzeWithFFT() {
  var numChannels = 1; // mono
  var duration = 1; // seconds
  var sampleRate = 44100; // Any value in [22050, 96000] is allowed
  var chirp = linearChirp(10000, 20000, duration, sampleRate);
  var numSamples = chirp.length;

  // Now we create the offline context to render this with.
  var ctx = new OfflineAudioContext(numChannels, numSamples, sampleRate);
  
  // Our example wires up an analyzer node in between source and destination.
  // You may or may not want to do that, but if you can follow how things are
  // connected, it will at least give you an idea of what is possible.
  //
  // This is what computes the spectrum (FFT) information for us.
  var analyser = ctx.createAnalyser();

  // There are abundant examples of how to get audio from a URL or the
  // microphone. This one shows you how to create it programmatically (we'll
  // use the chirp array above).
  var source = ctx.createBufferSource();
  var chirpBuffer = ctx.createBuffer(numChannels, numSamples, sampleRate);
  var data = chirpBuffer.getChannelData(0); // first and only channel
  for (var i = 0; i < numSamples; i++) {
    data[i] = 128 + Math.floor(chirp[i] * 127); // quantize to [0,256)
  }
  source.buffer = chirpBuffer;

  // Now we wire things up: source (data) -> analyser -> offline destination.
  source.connect(analyser);
  analyser.connect(ctx.destination);

  // When the audio buffer has been processed, this will be called.
  ctx.oncomplete = function(event) {
    console.log("audio processed");
    // To get the spectrum data (e.g., if you want to plot it), you use this.
    var frequencyBins = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(frequencyBins);
    console.log(frequencyBins);
    // You can also get the result of any filtering or any other stage here:
    console.log(event.renderedBuffer);
  };

  // Everything is now wired up - start the source so that it produces a
  // signal, and tell the context to start rendering.
  //
  // oncomplete above will be called when it is done.
  source.start();
  ctx.startRendering();
}
Telion
  • 727
  • 2
  • 10
  • 22
shiblon
  • 1,916
  • 1
  • 14
  • 5