为网络音频创建音量控制

create volume control for web audio

本文关键字:音量控制 创建 音频 网络      更新时间:2023-09-26

所以我正在通过网络音频创建一架钢琴,并且在实现音量控制时遇到了问题。每当单击某个键时,音量控制应规定其播放音量。我使用了来自html5rocks的代码,并将其修改为我自己的用途。基本上,我把所有的声音片段都加载到BUFFERS数组中,而不是VolumeSample数组。每当我试图操纵滑块并更改剪辑的增益时,我都会得到null的"无法读取属性"增益。我正在通过调试器测试它,一切都运行良好,直到this.gainNode.gain.value=fraction*fraction;我代码的一部分。只要看看我的代码,希望你能看到我缺少的东西。我想提醒大家注意playSounds(缓冲区)方法,它是创建和连接增益节点的地方,而底部的方法changeVolume是增益节点实际变化的地方:

    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;
};

这是我创建钢琴的地方,检查点击了哪个键并播放了合适的声音(单独的JS文件):

// 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]);
        }
}; 

以下是我在HTML中为音量控制定义范围滑块的地方(不用担心,它在我的代码中格式正确):

<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>

这个问题似乎是键盘本身使用的音量控制无法检测要使用和修改的声音缓冲区。当你确切地知道要为哪个源调整音量时,你提供的代码就很好了,比如我的Beat1和Beat2(这些音量控制都很好)。我需要能够修改缓冲区阵列中任何源的卷的代码。我正在使用Raphael软件包来创建键盘,如果这有帮助的话(可能没有)。我想提醒大家注意playSound(buffer)方法和VolumeMain.changeVolume函数。LowPFilter方法也不起作用,但一旦我们找到了如何调整任何给定源的音量,该方法的问题也将得到解决。

编辑(更新)。这将删除错误,并允许您访问gainNode值

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
};

以前的回复

我真的不理解这个问题,但如果你只想用一段代码作为用HTML范围滑块设置增益节点的例子,这里有一个振荡器的例子。你可能想做一个小的尖峰测试,看看这样的东西是否能在你的代码中使用振荡器,然后试着把它应用到你的音频缓冲区代码中。

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;
});