Reputation: 39
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
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.
<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