javascriptaudiosignal-processingweb-audio-api

custom wave forms in web audio API


I'm working through this awesome article: https://jackschaedler.github.io/circles-sines-signals/dft_introduction.html

I want to use the Web Audio API's PeriodicWave object to implement this demo: enter image description here

However, when I set a periodic wave with these settings:

 var real = new Float32Array([0,0,1,0,1]);
 var imag = new Float32Array(real.length);
 var customWave = context.createPeriodicWave(real,imag);
 osc.setPeriodicWave(customWave);

I output a wave that looks like this:

enter image description here Here is full code: http://jsbin.com/zaqojavixo/4/edit To see the waveform, please press play the sound a few times.

I believe these should match up, so here are my questions:

  1. Am I missing something fundamental about the theory here or am I just implementing it incorrectly? Is the PeriodicWave object supposed to do the same thing as illustrated in the article?
  2. If I am taking the wrong approach, how would I implement this diagram in Web Audio API? I have been able to match below by connecting two different sine waves of different frequencies to the same gain node - how is this different than using the PeriodicWave object?
  3. I'm new to DSP and Web Audio API - any suggested reading would be appreciated!
  4. Secondarily, in my example, I have to push the 'play the sound' button a couple of times before correct data is drawn to the canvas - the analyser seems to be behind the oscillator, even though analyser.getFloatTimeDomainData() is called after I start the oscillator any thoughts on what's going on here?

Edit: As noted in comments, my graph is upside down (on the canvas 0,0 is the upper left corner).


Solution

  • Note that the first array defines the cosine terms, the second the sine terms:

    The real parameter represents an array of cosine terms (traditionally the A terms). In audio terminology, the first element (index 0) is the DC-offset of the periodic waveform. The second element (index 1) represents the fundamental frequency. The third element represents the first overtone, and so on. The first element is ignored and implementations must set it to zero internally.

    The imag parameter represents an array of sine terms (traditionally the B terms). The first element (index 0) should be set to zero (and will be ignored) since this term does not exist in the Fourier series. The second element (index 1) represents the fundamental frequency. The third element represents the first overtone, and so on.

    Source

    You will see you get the expected waveform but "reversed" (drawn upside down thanks to @Julian for pointing that out in his answer - fixed below):

    snap

    (I inlined your code here with the arrays swapped around:)
    updated fixed drawing issues in original code

    //setup audio context
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    var context = new window.AudioContext();
    
    //create nodes
    var osc; //create in event listener so we can press the button more than once
    var masterGain = context.createGain();
    var analyser = context.createAnalyser();
    
    //routing
    masterGain.connect(analyser);
    analyser.connect(context.destination);
    
    var isPlaying = false;
    
    //draw function for canvas
    function drawWave(analyser, ctx) {
      
      var buffer = new Float32Array(1024),
          w = ctx.canvas.width;
      
      ctx.strokeStyle = "#777";
      ctx.setTransform(1,0,0,-1,0,100.5); // flip y-axis and translate to center
      ctx.lineWidth = 2;
      
      (function loop() {
        analyser.getFloatTimeDomainData(buffer);
        
        ctx.clearRect(0, -100, w, ctx.canvas.height);
    
        ctx.beginPath();
        ctx.moveTo(0, buffer[0] * 90);
        for (var x = 2; x < w; x += 2) ctx.lineTo(x, buffer[x] * 90);
        ctx.stroke();
        
        if (isPlaying) requestAnimationFrame(loop)
      })();
    }
    
    //button trigger
    $(function() {  
      var c = document.getElementById('scope'),
          ctx = c.getContext("2d");
      
      c.height = 200;
      c.width = 600;
      
      // make 0-line permanent as background
      ctx.moveTo(0, 100.5);
      ctx.lineTo(c.width, 100.5);
      ctx.stroke();
      c.style.backgroundImage = "url(" + c.toDataURL() + ")";
      
      $('button').on('mousedown', function() {
        osc = context.createOscillator();
        //osc settings
        osc.frequency.value = 220;
        var imag= new Float32Array([0,0,1,0,1]);   // sine
        var real = new Float32Array(imag.length);  // cos
        var customWave = context.createPeriodicWave(real, imag);  // cos,sine
        osc.setPeriodicWave(customWave);
    
        osc.connect(masterGain);
        osc.start();
        isPlaying = true;
        
        drawWave(analyser, ctx);
      });
    
      $('button').on('mouseup', function() {
        isPlaying = false;
        osc.stop();
      }); 
    });
    button {position:fixed;left:10px;top:10px}
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <button>Play the sound</button>
    <canvas id='scope'></canvas>