TeamRival
TeamRival

Reputation: 39

create volume control for web audio

So I am creating a piano through web audio and am having trouble implementing a volume control. Whenever a key is clicked, the volume control should dictate at what volume it is played through. I have used the code from html5rocks and modified it to my own uses. Basically instead of a VolumeSample array I have all of my soundclips loaded into a BUFFERS array. Whenever I try to manipulate the slider and change the gain of the clip, I get an 'cannot read property 'gain' of null. I am testing it through the debugger and everything runs fine up until the this.gainNode.gain.value = fraction * fraction; portion of my code. Just take a look at my code and hopefully you can see what I am missing. I'd like to call attention to the playSounds(buffer) method, which is where create and connect the gain node, and the method changeVolume at the bottom, which is where the actualy change in gain node happens:

    var context;
    var bufferLoader;
    var BUFFERS = {};
    var VolumeMain = {};
    var LowPFilter = {FREQ_MUL: 7000,
                  QUAL_MUL: 30};


    var BUFFERS_TO_LOAD = {
    Down1: 'mp3/0C.mp3',
    Down2: 'mp3/0CS.mp3',
    Down3: 'mp3/0D.mp3',
    Down4: 'mp3/0DS.mp3',
    Down5: 'mp3/0E.mp3',
    Down6: 'mp3/0F.mp3',
    Down7: 'mp3/0FS.mp3',
    Down8: 'mp3/0G.mp3',
    Down9: 'mp3/0GS.mp3',
    Down10: 'mp3/0A.mp3',
    Down11: 'mp3/0AS.mp3',
    Down12: 'mp3/0B.mp3',
    Up13: 'mp3/1C.mp3',
    Up14: 'mp3/1CS.mp3',
    Up15: 'mp3/1D.mp3',
    Up16: 'mp3/1DS.mp3',
    Up17: 'mp3/1E.mp3',
    Up18: 'mp3/1F.mp3',
    Up19: 'mp3/1FS.mp3',
    Up20: 'mp3/1G.mp3',
    Up21: 'mp3/1GS.mp3',
    Up22: 'mp3/1A.mp3',
    Up23: 'mp3/1AS.mp3',
    Up24: 'mp3/1B.mp3',
    Beat1: 'mp3/beat1.mp3',
        Beat2: 'mp3/beat2.mp3'
    };



    function loadBuffers() {
    var names = [];
    var paths = [];
    for (var name in BUFFERS_TO_LOAD) {
    var path = BUFFERS_TO_LOAD[name];
    names.push(name);
    paths.push(path);
    }
    bufferLoader = new BufferLoader(context, paths, function(bufferList) {
    for (var i = 0; i < bufferList.length; i++) {
      var buffer = bufferList[i];
      var name = names[i];
      BUFFERS[name] = buffer;
     }
    });
    bufferLoader.load();
    }
    document.addEventListener('DOMContentLoaded', function() {
    try {
    // Fix up prefixing
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    context = new AudioContext();
    }
    catch(e) {
    alert("Web Audio API is not supported in this browser");
    }
    loadBuffers();
    });




    function playSound(buffer) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    var filter1 = context.createBiquadFilter();
    filter1.type = 0;
    filter1.frequency.value = 5000;
    var gainNode = context.createGain();
    source.connect(gainNode);
    source.connect(filter1);
    gainNode.connect(context.destination);
    filter1.connect(context.destination);
    source.start(0);
    }
    //volume control
    VolumeMain.gainNode = null;
    VolumeMain.changeVolume = function(element) {
    var volume = element.value;
    var fraction = parseInt(element.value) / parseInt(element.max);
    this.gainNode.gain.value = fraction * fraction; //error occurs here
    };




// Start off by initializing a new context.
context = new (window.AudioContext || window.webkitAudioContext)();

if (!context.createGain)
  context.createGain = context.createGainNode;
if (!context.createDelay)
  context.createDelay = context.createDelayNode;
if (!context.createScriptProcessor)
  context.createScriptProcessor = context.createJavaScriptNode;

// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
return  window.requestAnimationFrame       || 
  window.webkitRequestAnimationFrame || 
  window.mozRequestAnimationFrame    || 
  window.oRequestAnimationFrame      || 
  window.msRequestAnimationFrame     || 
  function( callback ){
  window.setTimeout(callback, 1000 / 60);
};
})();




function BufferLoader(context, urlList, callback) {
  this.context = context;
  this.urlList = urlList;
  this.onload = callback;
  this.bufferList = new Array();
  this.loadCount = 0;
}

BufferLoader.prototype.loadBuffer = function(url, index) {
  // Load buffer asynchronously
  var request = new XMLHttpRequest();
  request.open("GET", url, true);
  request.responseType = "arraybuffer";

  var loader = this;

  request.onload = function() {
    // Asynchronously decode the audio file data in request.response
    loader.context.decodeAudioData(
      request.response,
      function(buffer) {
        if (!buffer) {
          alert('error decoding file data: ' + url);
          return;
        }
        loader.bufferList[index] = buffer;
        if (++loader.loadCount == loader.urlList.length)
          loader.onload(loader.bufferList);
      },
      function(error) {
        console.error('decodeAudioData error', error);
      }
    );
  }

  request.onerror = function() {
    alert('BufferLoader: XHR error');
  }

  request.send();
};

BufferLoader.prototype.load = function() {
  for (var i = 0; i < this.urlList.length; ++i)
  this.loadBuffer(this.urlList[i], i);
}
  LowPFilter.changeFrequency = function(element) {
  // Clamp the frequency between the minimum value (40 Hz) and half of the
  // sampling rate.
  var minValue = 40;
  var maxValue = context.sampleRate / 2;
  // Logarithm (base 2) to compute how many octaves fall in the range.
  var numberOfOctaves = Math.log(maxValue / minValue) / Math.LN2;
  // Compute a multiplier from 0 to 1 based on an exponential scale.
  var multiplier = Math.pow(2, numberOfOctaves * (element.value - 1.0));
  // Get back to the frequency value between min and max.
  this.filter1.frequency.value = maxValue * multiplier;
};

LowPFilter.changeQuality = function(element) {
  this.filter1.Q.value = element.value * this.QUAL_MUL;
};

LowPFilter.toggleFilter = function(element) {
  this.source.disconnect(0);
  this.filter1.disconnect(0);
  // Check if we want to enable the filter.
  if (element.checked) {
    // Connect through the filter.
    this.source.connect(this.filter1);
    this.filter1.connect(context.destination);
  } else {
    // Otherwise, connect directly.
    this.source.connect(context.destination);
  }
};
function Beat1() {
  this.isPlaying = false;
};

Beat1.prototype.play = function() {
  this.gainNode = context.createGain();
  this.source = context.createBufferSource();
  this.source.buffer = BUFFERS.Beat1;

  // Connect source to a gain node
  this.source.connect(this.gainNode);
  // Connect gain node to destination
  this.gainNode.connect(context.destination);
  // Start playback in a loop
  this.source.loop = true;
  this.source[this.source.start ? 'start' : 'noteOn'](0);
};

Beat1.prototype.changeVolume = function(element) {
  var volume = element.value;
  var fraction = parseInt(element.value) / parseInt(element.max);
  // Let's use an x*x curve (x-squared) since simple linear (x) does not
  // sound as good.
  this.gainNode.gain.value = fraction * fraction;
};

Beat1.prototype.stop = function() {
  this.source[this.source.stop ? 'stop' : 'noteOff'](0);
};

Beat1.prototype.toggle = function() {
  this.isPlaying ? this.stop() : this.play();
  this.isPlaying = !this.isPlaying;
};

function Beat2() {
  this.isPlaying = false;
};

Beat2.prototype.play = function() {
  this.gainNode = context.createGain();
  this.source = context.createBufferSource();
  this.source.buffer = BUFFERS.Beat2;

  // Connect source to a gain node
  this.source.connect(this.gainNode);
  // Connect gain node to destination
  this.gainNode.connect(context.destination);
  // Start playback in a loop
  this.source.loop = true;
  this.source[this.source.start ? 'start' : 'noteOn'](0);
};

Beat2.prototype.changeVolume = function(element) {
  var volume = element.value;
  var fraction = parseInt(element.value) / parseInt(element.max);
  // Let's use an x*x curve (x-squared) since simple linear (x) does not
  // sound as good.
  this.gainNode.gain.value = fraction * fraction;
};

Beat2.prototype.stop = function() {
  this.source[this.source.stop ? 'stop' : 'noteOff'](0);
};

Beat2.prototype.toggle = function() {
  this.isPlaying ? this.stop() : this.play();
  this.isPlaying = !this.isPlaying;
};

This is where I create the piano and check which key was clicked and play the appropriate sound (seperate JS file):

// keyboard creation function
window.onload = function () {   
    // Keyboard Height
    var keyboard_height = 120;

    // Keyboard Width
    var keyboard_width = 980;

    // White Key Color
    var white_color = 'white';

    // Black Key Color
    var black_color = 'black';

    // Number of octaves
    var octaves = 2;

    // ID of containing Div
    var div_id = 'keyboard';

    //------------------------------------------------------------

    var paper = Raphael(div_id, keyboard_width, keyboard_height);

    // Define white key specs
    var white_width = keyboard_width / 14;

    // Define black key specs
    var black_width = white_width/2;
    var black_height = keyboard_height/1.6;

    var repeat = 0;
    var keyboard_keys = [];

    //define white and black key names
    var wkn = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
    var bkn = ['Csharp', 'Dsharp', 'Fsharp', 'Gsharp', 'Asharp'];

    //create octave groups
    for (i=0;i<octaves;i++) {


        //create white keys first
        for (var w=0; w <= 6 ; w++) {
            keyboard_keys[wkn[w]+i] = paper.rect(white_width*(repeat + w), 0, white_width, keyboard_height).attr("fill", white_color);
        };

        //set multiplier for black key placement
        var bw_multiplier = 1.5;

        //then black keys on top
        for (var b=0; b <= 4 ; b++) {   
            keyboard_keys[bkn[b]+i] = paper.rect((white_width*repeat) + (black_width*bw_multiplier), 0, black_width, black_height).attr("fill", black_color);
            bw_multiplier = (b == 1) ? bw_multiplier + 4 : bw_multiplier + 2;
        };

        repeat = repeat + 7;
    }


        for (var i in keyboard_keys) {

            (function (st) {
                st.node.onclick = function(event) {
                    var newColor = '#'+(0x1000000+(Math.random())*0xffffff).toString(16).substr(1,6);
                    st.animate({fill:newColor}, 100);
                    var testKey = st.paper.getElementByPoint(event.pageX, event.pageY);
                    var indexOfKey = testKey.id;
                    if (indexOfKey == 0)
                    {
                        playSound(BUFFERS.Down1);
                    }
                    else if (indexOfKey == 1)
                    {
                        playSound(BUFFERS.Down3);
                    }
                    else if (indexOfKey == 2)
                    {
                        playSound(BUFFERS.Down5);
                    }
                    else if (indexOfKey == 3)
                    {
                        playSound(BUFFERS.Down6);
                    }
                    else if (indexOfKey == 4)
                    {
                        playSound(BUFFERS.Down8);
                    }
                    else if (indexOfKey == 5)
                    {
                        playSound(BUFFERS.Down10);
                    }
                    else if (indexOfKey == 6)
                    {
                        playSound(BUFFERS.Down12);
                    }
                    else if (indexOfKey == 7)
                    {
                        playSound(BUFFERS.Down2);
                    }
                    else if (indexOfKey == 8)
                    {
                        playSound(BUFFERS.Down4);
                    }
                    else if (indexOfKey == 9)
                    {
                        playSound(BUFFERS.Down7);
                    }
                    else if (indexOfKey == 10)
                    {
                        playSound(BUFFERS.Down9);
                    }
                    else if (indexOfKey == 11)
                    {
                        playSound(BUFFERS.Down11);
                    }
                    else if (indexOfKey == 12)
                    {
                        playSound(BUFFERS.Up13);
                    }
                    else if (indexOfKey == 13)
                    {
                        playSound(BUFFERS.Up15);
                    }
                    else if (indexOfKey == 14)
                    {
                        playSound(BUFFERS.Up17);
                    }
                    else if (indexOfKey == 15)
                    {
                        playSound(BUFFERS.Up18);
                    }
                    else if (indexOfKey == 16)
                    {
                        playSound(BUFFERS.Up20);
                    }
                    else if (indexOfKey == 17)
                    {
                        playSound(BUFFERS.Up22);
                    }
                    else if (indexOfKey == 18)
                    {
                        playSound(BUFFERS.Up24);
                    }
                    else if (indexOfKey == 19)
                    {
                        playSound(BUFFERS.Up14);
                    }
                    else if (indexOfKey == 20)
                    {
                        playSound(BUFFERS.Up16)
                    }
                    else if (indexOfKey == 21)
                    {
                        playSound(BUFFERS.Up19);
                    }
                    else if (indexOfKey == 22)
                    {
                        playSound(BUFFERS.Up21);
                    }
                    else
                    {
                        playSound(BUFFERS.Up23);
                    }
                };
            })(keyboard_keys[i]);
        }
}; 

Here's where I define the range slider for the volume control in my HTML (don't worry it is formatted correctly on in my code):

<div id="keyboard">
                    <script>
                    loadBuffers();
                    var beat1 = new Beat1();
                    var beat2 = new Beat2();
                    </script>
                </div>

                <div>Volume: <input type="range" min="0" max="100" value="100" oninput="VolumeMain.changeVolume(this);" /></div>
                <div>Low Pass Filter on: <input type="checkbox" checked="false" oninput="LowPFilter.toggleFilter(this);" />
                Frequency: <input type="range" min="0" max="1" step="0.01" value="1" oninput="LowPFilter.changeFrequency(this);" />
                Quality: <input type="range" min="0" max="1" step="0.01" value="0" oninput="LowPFilter.changeQuality(this);" /></div>
                <div>Beat 1: <input type="button" onclick="beat1.toggle();" value="Play/Pause"/>
                      Volume: <input type="range" min="0" max="100" value="100" onchange="beat1.changeVolume(this);"></div>
                <div>Beat 2: <input type="button" onclick="beat2.toggle();" value="Play/Pause"/>
                      Volume: <input type="range" min="0" max="100" value="100" onchange="beat2.changeVolume(this);"></div>
    </div>

This issue seems to be that the Volume control being used for the keyboard itself is somehow not being able to detect which sound buffer to use and modify. The code you supplied is good when you know exactly which source you are going to be adjusting the volume for, like in the case of my Beat1 and Beat 2 (those volume controls both work fine). I need the code to be able to modify the volume of any source in the buffer array. I'm using the Raphael package to create the keyboard, if that helps (it probably doesn't). I would call attention to the playSound(buffer) method and the VolumeMain.changeVolume functions. None of the LowPFilter methods work either but once we figure out how to adjust the volume for any given source that method's problem will also be fixed.

Upvotes: 3

Views: 4187

Answers (1)

William
William

Reputation: 4578

Edit (update). This removes the error and allows you to access the gainNode value

var gainNode = context.createGain();

function playSound(buffer) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    var filter1 = context.createBiquadFilter();
    filter1.type = 0;
    filter1.frequency.value = 5000;
    source.connect(gainNode);
    source.connect(filter1);
    gainNode.connect(context.destination);
    filter1.connect(context.destination);
    source.start(audioContext.currentTime);
}


//volume control

VolumeMain.changeVolume = function(element) {
    var volume = element.value;
    var fraction = parseInt(element.value) / parseInt(element.max);
    gainNode.gain.value = fraction * fraction;

    console.log(gainNode.gain.value);      // Console log of gain value when slider is moved
};

Previous reply

I don't really understand the problem but if you just want a piece of code as an example of setting up a gain node with an HTML range slider here's an example with an oscillator. You might want to do a little spike test and see if something like this works in your code with an oscillator and then try and apply it to your audio buffer code.

http://jsfiddle.net/vqb9dmrL/

<input id="gainSlider" type="range" min="0" max="1" step="0.05" value="0.5"/>

var audioContext = new webkitAudioContext();



var osc = audioContext.createOscillator();
osc.start(audioContext.cueentTime);


var gainChan1 = audioContext.createGain();
osc.connect(gainChan1);
gainChan1.connect(audioContext.destination);   


var gainSlider = document.getElementById("gainSlider");
gainSlider.addEventListener('change', function() {
gainChan1.gain.value = this.value;
}); 

Upvotes: 3

Related Questions