【问题标题】:Web audio oscillator is clicking in Firefox onlyWeb 音频振荡器仅在 Firefox 中单击
【发布时间】:2016-05-02 05:22:45
【问题描述】:

我正在尝试使用网络音频振荡器创建一个简单的节拍器,因此不需要外部音频文件。我通过非常快速地上下调节振荡器的音量来创建节拍器的声音(因为您不能多次使用 start() 和 stop()),然后以设定的间隔重复该功能。它最终听起来像一个漂亮的小木块。

以下代码在 Chrome、Safari 和 Opera 中运行/听起来很棒。但是在 Firefox 中,当音量增大时会出现令人讨厌的间歇性“点击”。我已经尝试改变攻击/释放时间来摆脱咔嗒声,但它们必须非常非常长才能持续消失。事实上,如此长的振荡器听起来就像一个持续的音符。

var audio = new (window.AudioContext || window.webkitAudioContext)();
var tick = audio.createOscillator();
var tickVol = audio.createGain();

tick.type = 'sine'; 
tick.frequency.value = 1000;
tickVol.gain.value = 0; //setting the volume to 0 before I connect everything
tick.connect(tickVol);
tickVol.connect(audio.destination);
tick.start(0);

var metronome = {
    start: function repeat() {
        now = audio.currentTime;

        //Make sure volume is 0 and that no events are changing it
        tickVol.gain.cancelScheduledValues(now);
        tickVol.gain.setValueAtTime(0, now);

        //Play the osc with a super fast attack and release so it sounds like a click
        tickVol.gain.linearRampToValueAtTime(1, now + .001);
        tickVol.gain.linearRampToValueAtTime(0, now + .001 + .01);

        //Repeat this function every half second
        click = setTimeout(repeat, 500);
    },
    stop: function() {
        if(typeof click !== 'undefined') {
            clearTimeout(click);
            tickVol.gain.value = 0;
        }
    }
}

$("#start").click(function(){
  metronome.start();
});

$("#stop").click(function(){
  metronome.stop();
});

Codepen

有没有办法让 FF 听起来像其他 3 个浏览器?

【问题讨论】:

    标签: javascript firefox web-audio-api


    【解决方案1】:

    最好让 Firefox 来解决这个问题(如果它确实是一个带有自动化功能的 Firefox 错误)。话虽如此,您可能可以通过使用具有所需预计算点击波形的 AudioBufferSource 节点使所有浏览器保持一致。只需生成一个正弦波,根据需要(手动)将其向上和向下倾斜并定期播放。

    不是很好,但应该是跨平台的。

    【讨论】:

    【解决方案2】:

    我在最新的 Opera 中遇到了完全相同的问题,发现问题出在单个声音的“十进制时间长度”上。

    我写了一个莫尔斯电码翻译器,和你的一样,它只是通过 createOscillator 创建的一系列简单的短声音/哔声。

    使用摩尔斯电码,您可以根据 5 个字母长的单词(如 codex 或 paris)计算速度计数(每分钟字数)。

    要让每分钟 20 或 30 次巴黎的比赛准确地完成,我必须使用合理的时间长度,例如 0.61。在 Opera 中,这会导致“声音点击结束”。将其更改为 0.6 后,点击在所有浏览器中都消失了 - 除了 Firefox。

    我在声音之间尝试了 freq = 0 和 gain = 0,但仍然在 FF 中获得了最后的点击,我对 Web Audio 的了解还不够,无法尝试其他任何东西。

    另外,我注意到您正在使用循环和超时来到达下一个刻度。您是否尝试过“振荡器 onended 功能”?我已经将它与一个简单的计数器增量和可变长度的空白声音/音符一起使用。想看的话就去我的JS的最后吧。

    **UPDATE - 我一直在摆弄 setValueAtTime() 和 linearRampToValueAtTime() 并且似乎已经解决了点击问题。滚动到脚本底部以查看示例。 **

    (function(){
    
    /* Morse Code Generator & Translator - Kurt Grigg 2003 (Updated for sound and CSS3) */
    
    var d = document;
    d.write('<div class="Mcontainer">'
    +'<div class="Mtitle">Morse Code Generator Translator</div>'
    +'<textarea id="txt_in" class="Mtxtarea"></textarea>'
    +'<div class="Mtxtareatitle">Input</div>'
    +'<textarea id="txt_out" class="Mtxtarea" style="top: 131px;"></textarea>'
    +'<div class="Mtxtareatitle" style="top: 172px;">Output</div>'
    +'<div class="Mbuttonwrap">'
    +'<input type="button" class="Mbuttons" id="how" value="!">'
    +'<input type="button" class="Mbuttons" id="tra" value="translate">'
    +'<input type="button" class="Mbuttons" id="ply" value="play">'
    +'<input type="button" class="Mbuttons" id="pau" value="pause">'
    +'<input type="button" class="Mbuttons" id="res" value="reset"></div>'
    +'<select id="select" class="Mselect">' 
    +'<option value=0.07 selected="selected">15 wpm</option>'
    +'<option value=0.05>20 wpm</option>'
    +'<option value=0.03>30 wpm</option>'
    +'</select>'
    +'<div class="sliderWrap">volume <input id="volume" type="range" min="0" max="1" step="0.01" value="0.05"/></div>'
    +'<div class="Mchckboxwrap">'
    +'<span style="text-align: right;">separator <input type="checkbox" id="slash" class="Mchckbox"></span>'
    +'</div>'
    +'<div id="about" class="Minfo">'
    +'<b>Input morse</b><br>'
    +'<ul><li>Enter morse into input box using full stop (period) and minus sign (hyphen)</li>'
    +'<li>Morse letters must be separated by 1 space</li>'
    +'<li>Morse words must be separated by 3 or more spaces</li>'
    +'<li>You can use / to separate morse words. There must be at least 1 space before and after each separator used</li>'
    +'</ul>'
    +'<b>Input text</b><br>'
    +'<ul class="Mul"><li>Enter text into input box</li>'
    +'<li>Characters that cannot be translated will be ignored</li>'
    +'<li>If morse and text is entered, the converter will assume morse mode</li></ul>'
    +'<input type="button" value="close" id="clo" class="Mbuttons">'
    +'</div><div id="mdl" class="modal"><div id="bdy"><div id="modalMsg">A MSG</div><input type="button" value="close" id="cls" class="Mbuttons"></div></div></div>');
    
    var ftmp = d.getElementById('mdl');
    var del;
    
    d.getElementById('tra').addEventListener("click", function(){convertToAndFromMorse(txtIn.value);},false);
    d.getElementById('ply').addEventListener("click", function(){CancelIfPlaying();},false);
    d.getElementById('pau').addEventListener("click", function(){stp();},false);
    d.getElementById('res').addEventListener("click", function(){Rst();txtIn.value = '';txtOt.value = '';},false);
    
    
    d.getElementById('how').addEventListener("click", function(){msgSelect();},false);
    d.getElementById('clo').addEventListener("click", function(){fadeOut();},false);
    
    d.getElementById('cls').addEventListener("click", function(){fadeOut();},false);
    d.getElementById('bdy').addEventListener("click", function(){errorSelect();},false);
    
    var wpm = d.getElementById('select');
    wpm.addEventListener("click", function(){wpMin()},false);
    
    var inc = 0;
    var playing = false; 
    var txtIn = d.getElementById('txt_in');
    var txtOt = d.getElementById('txt_out');
    var paused = false;
    var allowed = ['-','.',' '];
    var aud;
    var tmp = (window.AudioContext || window.webkitAudioContext)?true:false;
    if (tmp) {
        aud = new (window.AudioContext || window.webkitAudioContext)();
    }
    var incr = 0;
    var speed = parseFloat(wpm.options[wpm.selectedIndex].value);
    var char = [];
    var alphabet = [["A",".-"],["B","-..."],["C","-.-."],["D","-.."],["E","."],["F","..-."],["G","--."],["H","...."],["I",".."],["J",".---"],
        ["K","-.-"],["L",".-.."],["M","--"],["N","-."],["O","---"],["P",".--."],["Q","--.-"],["R",".-."],["S","..."],["T","-"],["U","..-"],
        ["V","...-"],["W",".--"],["X","-..-"],["Y","-.--"],["Z","--.."],["1",".----"],["2","..---"],["3","...--"],["4","....-"],["5","....."],
        ["6","-...."],["7","--..."],["8","---.."],["9","----."],["0","-----"],[".",".-.-.-"],[",","--..--"],["?","..--.."],["'",".----."],["!","-.-.--"],
        ["/","-..-."],[":","---..."],[";","-.-.-."],["=","-...-"],["-","-....-"],["_","..--.-"],["\"",".-..-."],["@",".--.-."],["(","-.--.-"],[" ",""]];
    
    function errorSelect() {
        txtIn.focus();
    }
    
    function modalSwap(msg) {
        d.getElementById('modalMsg').innerHTML = msg;
    }
    
    function msgSelect() {
        ftmp = d.getElementById('about');
        fadeIn(); 
    }
    
    function fadeIn() {
        ftmp.removeEventListener("transitionend", freset);
        ftmp.style.display = "block";
        del = setTimeout(doFadeIn,100);
    }
    
    function doFadeIn() {
        clearTimeout(del);
        ftmp.style.transition = "opacity 0.5s linear";
        ftmp.style.opacity = "1";
    }
    
    function fadeOut() {
        ftmp.style.transition = "opacity 0.8s linear";
        ftmp.style.opacity = "0";
        ftmp.addEventListener("transitionend",freset , false);
    }
    
    function freset() {
        ftmp.style.display = "none";
        ftmp.style.transition = "";
        ftmp = d.getElementById('mdl');
    }
    
    function stp() {
        paused = true;
    }
    
    function wpMin() {
        speed = parseFloat(wpm.options[wpm.selectedIndex].value);
    }
    
    function Rst(){ 
        char = [];
        inc = 0;
        playing = false;
        paused = false;
    }
    
    function CancelIfPlaying(){
        if (window.AudioContext || window.webkitAudioContext) {paused = false;
            if (!playing) { 
                IsReadyToHear();
            }
            else {
                return false;
            }
        }
        else {
            modalSwap("<p>Your browser doesn't support Web Audio API</p>");
            fadeIn();
            return false;
        }
    }
    
    function IsReadyToHear(x){
        if (txtIn.value == "" || /^\s+$/.test(txtIn.value)) {
            modalSwap('<p>Nothing to play, enter morse or text first</p>');
            fadeIn();
            txtIn.value = '';
            return false;
        }
        else if (char.length < 1 && (x != "" || !/^\s+$/.test(txtIn.value)) && txtIn.value.length > 0) {
            modalSwap('<p>Click Translate button first . . .</p>');
            fadeIn();
            return false;
        }
        else{
            playMorse();
        }
    }
    
    function convertToAndFromMorse(x){
        var swap = [];
        var outPut = "";
        x = x.toUpperCase();
    
        /* Is input empty or all whitespace? */
        if (x == '' || /^\s+$/.test(x)) {
            modalSwap("<p>Nothing to translate, enter morse or text</p>");
            fadeIn();
            txtIn.value = '';
            return false;
        }
    
        /* Remove front & end whitespace */
        x = x.replace(/\s+$|^\s*/gi, ''); 
        txtIn.value = x;
        txtOt.value = "";
    
        var isMorse = (/(\.|\-)\.|(\.|\-)\-/i.test(x));// Good enough.
    
        if (!isMorse){
            for (var i = 0; i < alphabet.length; i++){
                swap[i] = [];
                for (var j = 0; j < 2; j++){
                    swap[i][j] = alphabet[i][j].replace(/\-/gi, '\\-');
                }
            }
        }
    
        var swtch1 = (isMorse) ? allowed : swap;
        var tst = new RegExp( '[^' + swtch1.join('') + ']', 'g' ); 
        var swtch2 = (isMorse)?' ':'';
        x = x.replace( tst, swtch2);  //remove unwanted chars.
        x = x.split(swtch2); 
    
        if (isMorse) {
            var tidy = [];
            for (var i = 0; i < x.length; i++){
                if ((x[i] != '') || x[i+1] == '' && x[i+2] != '') {
                    tidy.push(x[i]);
                }
            }
        }
    
        var swtch3 = (isMorse) ? tidy : x;
    
        for (var j = 0; j < swtch3.length; j++) {
            for (var i = 0; i < alphabet.length; i++){
                if (isMorse) {
                    if (tidy[j] == alphabet[i][1]) {
                        outPut += alphabet[i][0];
                    } 
                } 
                else {
                    if (x[j] == alphabet[i][0]) {
                        outPut += alphabet[i][1] + ((j < x.length-1)?"  ":"");
                    }
                }
            }
        }
    
        if (!isMorse) {
            var wordDivide = (d.getElementById('slash').checked)?"  /  ":"     ";
            outPut = outPut.replace(/\s{3,}/gi, wordDivide);
        }
    
        if (outPut.length < 1) {
            alert('Enter valid text or morse...');
            txtIn.value = '';
        }
        else {
            txtOt.value = outPut;
        }
    
        var justMorse = (!isMorse) ? outPut : tidy;
    
        FormatForSound(justMorse);
    }
    
    function FormatForSound(s){
        var n = [];
        var b = '';
        if (typeof s == 'object') {
            for (var i = 0; i < s.length; ++i) {
                var f = (i == s.length-1)?'':'  ';
                var t = b += (s[i] + f);
            }
        }
        var c = (typeof s == 'object')? t : s;
        c = c.replace(/\//gi, '');
        c = c.replace(/\s{1,3}/gi, '4');
        c = c.replace(/\./gi, '03');
        c = c.replace(/\-/gi, '13');  
        c = c.split('');
        for (var i = 0; i < c.length; i++) {
            n.push(c[i]);
        }
        char = n;
    }
    
    function vlm() {
        return document.getElementById('volume').value;
    }
    
    function playMorse() {
    
        if (paused){ 
            playing = false;
            return false;
        }
    
        playing = true;
        if (incr >= char.length) {
            incr = 0;
            playing = false;
            paused = false;
            return false;
        }
        
        var c = char[incr];
        var freq = 550;
        var volume = (c < 2) ? vlm() : 0 ;
        var flen = (c == 0 || c == 3) ? speed : speed * 3;
    
        var osc = aud.createOscillator();
        osc.type = 'sine'; 
        osc.frequency.value = freq;
    
        var oscGain = aud.createGain();
        oscGain.gain.value = volume;
        osc.connect(oscGain);
        oscGain.connect(aud.destination);
    
        var now = aud.currentTime;
    
        osc.start(now);
    
            /*
            Sharp volume fade to stop harsh clicks if wave is stopped 
            at a point other than the (natural zero crossing point) 
            */
            oscGain.gain.setValueAtTime(volume, now + (flen*0.8));
            oscGain.gain.linearRampToValueAtTime(0.0, now + (flen*0.9999));
        
        
        osc.stop(now + flen);
    
        osc.onended = function() {
            incr++;
            playMorse();
        }
    }      
    })();
    body {
        text-align: center;  
    }
    
    
    
    
    
    .Mcontainer {
    display: inline-block;
    position: relative;
    width: 382px;
    height: 302px;
    border: 1px solid #000;
    border-radius: 6px;
    text-align: center;
    font: bold 11px sans-serif;
    background-color: rgb(203,243,65);
    box-shadow: 0px 4px 2px rgba(0,0,0,0.3);
    }
    .Mtitle {
    -webkit-user-select: none;   
    -moz-user-select: none;   
    display: inline-block;
    position: absolute;
    width: 380px;
    height: 20px;
    margin: auto;
    left: 0; right: 0;
    font-size: 16px;
    line-height: 20px;
    color:  #666;
    }
    .Mtxtareatitle {
    -webkit-user-select: none;   
    -moz-user-select: none; 
    display: block;
    position: absolute;
    top: 60px;
    left: -36px;
    height: 22px;
    width: 106px;
    font-size: 18px;
    line-height: 22px;
    text-align: center;
    color: #555;
    transform: rotate(-90deg);
    }
    .Mtxtarea {
    display: block;
    position: absolute;
    top: 18px;
    margin: auto;
    left: 0; right: 0;
    height: 98px;
    width: 344px;
    border: 0.5px solid #000;
    border-radius: 6px;
    padding-top: 6px;
    padding-left: 24px;
    resize: none;
    background-color: #fffff0;
    font: bold 10px courier;
    color: #555;
    text-transform: uppercase;
    overflow: auto;
    outline: 0;    box-shadow: inset 0px 2px 5px rgba(0,0,0,0.5);
    }
    .Minfo {
    display: none;
    position: absolute;
    top: -6px; left:-6px;
    padding: 6px;
    height: auto;
    width:  370px;
    text-align: left;
    border: 0.5px solid #000;
    border-radius: 6px;
    box-shadow: 0px 4px 2px rgba(0,0,0,0.3);
    background-color: rgb(203,243,65);
    font: 11px sans-serif;
    color: #555;
    opacity: 0;
    }
    .Mbuttonwrap {
    display: block;
    position: absolute;
    top: 245px;
    margin: auto;
    left: 0; right: 0;
    height: 26px;
    width: 100%;
    }
    .Mbuttons {
    display: inline-block;
    width: 69px;
    height: 22px;
    border: none;
    margin: 0px 3.1px 0px 3.1px;
    background-color: transparent;
    font: bold 11px sans-serif;
    color: #555;
    border-radius: 20px;
    cursor: pointer;
    box-shadow: 0px 2px 2px rgba(0,0,0,0.5);
    outline: 0;
    }
    .Mbuttons:hover {
    background-color:  rgb(213,253,75);
    }
    .Mbuttons:active {
    position: relative;
    top: 1px;
    box-shadow: 0px 1px 2px rgba(0,0,0,0.8);
    }
    .Mchckboxwrap {
    display: block;
    position: absolute;
    top: 274px;
    left: 289px;
    width: 87px;
    height: 21px;
    line-height: 22px;
    border: 0.5px solid #000;
    color: #555;
    background: #fff;
    -webkit-user-select: none;   
    -moz-user-select: none;   
    }
    .Mselect {
    display: block;
    position: absolute;
    top: 274px;
    left: 6px;
    width: 88px;
    height: 22px;
    border: 0.5px solid #000;
    padding-left: 5%;
    background: #fff;
    font: bold 11px sans-serif;
    color: #555;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    outline: 0;
    }
    ::selection {
    color: #fff;
    background: #555;
    }
    .Mchckbox {
    margin-top: 1px;
    vertical-align: middle;
    cursor: pointer;
    outline: 0;
    }
    .modal {
    display: none;
    position: absolute;
    margin: auto;
    top: 0;right: 0;bottom: 0;left: 0;
    background: rgba(0,0,0,0.5);
    -webkit-user-select: none;  
    -moz-user-select: none;
    opacity: 0;
    text-align: center;
    }
    .modal > div {   
    display: inline-block;
    position: relative;
    width: 250px;
    height: 70px;
    margin: 10% auto;
    padding: 10px;
    border: 0.5px solid #000;
    border-radius:6px;
    background-color: rgb(203,243,65);
    font: bold 11px sans-serif;
    color: #555;
    box-shadow: 4px 4px 2px rgba(0,0,0,0.3);
    text-align: center;
    }
    .sliderWrap {
    display: block;
    position: absolute;
    top: 274px;
    margin:auto;padding: 0;
    left: 0; right: 0;
    width: 184px;
    height: 21px;
    border: 0.5px solid #000;
    background: #fff;
    font: bold 11px sans-serif;
    color: #555;
    line-height: 21px;
    text-align: center;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    outline: 0;
    }
    input[type=range] {
    -webkit-appearance: none;
    width: 50%;
    margin: 0;padding: 0;
    vertical-align: middle;
    }
    input[type=range]:focus {
    outline: none;
    }
    input[type=range]::-webkit-slider-runnable-track {
    width: 100%;
    height: 4px;
    cursor: pointer;
    background: #666;
    }
    input[type=range]::-webkit-slider-thumb {
    box-shadow: 1px 1px 0.5px rgba(0, 0, 0, 0.5);
    border: none;
    height: 10px;
    width: 20px;
    border-radius: 5px;
    background: #ffffff;
    cursor: pointer;
    -webkit-appearance: none;
    margin-top: -3px;
    }
    input[type=range]:focus::-webkit-slider-runnable-track {
    background: #666;
    }
    input[type=range]::-moz-range-track {
    width: 100%;
    height: 4px;
    cursor: pointer;
    background: #666;
    }
    input[type=range]::-moz-range-thumb {
    box-shadow: 1px 1px 0.5px rgba(0, 0, 0, 0.5);
    height: 10px;
    width: 20px;
    border: none;
    border-radius: 5px;
    background: #ffffff;
    cursor: pointer;
    }
    input[type=range]::-ms-thumb {
    height: 10px;
    width: 20px;
    border: none;
    border-radius: 5px;
    background: #ffffff;
    box-shadow: 1px 1px 0.5px rgba(0, 0, 0, 0.5);
    cursor: pointer;
    }
    input[type=range]::-ms-track {
    width: 100%;
    height: 4px;
    cursor: pointer;
    background: transparent;
    border: 5px solid transparent;
    color: transparent;
    }
    input[type=range]::-ms-fill-lower {
    background: #666;
    }
    input[type=range]::-ms-fill-upper {
    background: #666;
    }
    ::-ms-tooltip {
    display: none;
    }
    select::-ms-expand {
    display: none;
    }

    【讨论】:

    • 是的,我观察到的是当十进制时间低于 0.05 秒时,FF 开始出现故障。不幸的是,我需要保持小以模拟节拍器的“咔哒”声,所以我没有太多的余地。但是,我确实向 Mozilla 提交了 bug
    • 我已经对此进行了一些阅读,不幸的是这不是一个错误!原来咔哒声的发生是因为我们在“自然过零”以外的点突然切断声波。哦,好吧,回到绘图板。
    【解决方案3】:

    AFAIK 这个问题并不是 Firefox 特有的,尽管查看您的代码,我不确定为什么它不会在其他浏览器中发生。

    问题在于,当您将 *rampToValueAtTime 安排到可听源时,当该源当前未在两个斜坡点之间进行插值时,会出现“咔哒”声,这可能是由于底层实现如何立即开始采用新的考虑斜坡点,即使它计划在未来发生。

    如果您在进行插值的两点之间安排一个新的斜坡点,也会听到咔哒声。

    我想出的解决方法是使用替代方法来逐渐更改 AudioParam 值 setTargetAtTime,或者将 AudioParam 的 value 属性设置为第一个斜坡点值。不是 setValueAtTime,而是在给定分支上发生任何可听见的事情之前分配给 value 属性本身。


    setTargetAtTime

    您既不需要 cancelScheduledValues 也不需要 setValueAtTime,只需两次调用 setTargetAtTime,这只是一个带有指定长度的指数插值的 setValueAtTime。

    var metronome = {
        start: function repeat() {
            now = audio.currentTime;
    
            //Play the osc with a super fast attack and release so it sounds like a click
            tickVol.gain.setTargetAtTime(1, now, 0.01);
            tickVol.gain.setTargetAtTime(0, now + 0.01, 0.01);
    
            //Repeat this function every half second
            click = setTimeout(repeat, 500);
        }
    }
    

    Live demo on JSFiddle

    【讨论】:

    • 感谢 John,您对 *rampToValueAtTime 行为的描述非常合理!不幸的是,您的小提琴在 FF 中不会为我产生任何声音(尽管它在 Chrome 中效果很好)。如果我将时间(在 JS 窗格的第 18 行)从 0.005 增加到 0.2,它将开始在 FF 中持续播放,但与我的第一个示例一样,点击仍然非常明显。您是否看到/听到相同的行为?另外,我尝试设置增益节点的初始value 以匹配第一个斜坡点值,但仍然得到相同的结果。
    • 直接设置 value 属性很快(如果还没有)将不起作用。在 Chrome 中,设置 value 属性会导致平滑。但是,规范正在更改,因此它的行为就像您执行了 setValueAtTime(value, now) 一样。如果您想要或需要平滑,您需要执行 setTargetAtTime()。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-29
    • 2013-09-11
    • 1970-01-01
    • 1970-01-01
    • 2020-07-12
    • 1970-01-01
    相关资源
    最近更新 更多