Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Web audio oscillator is clicking in Firefox only

I'm trying to create a simple metronome using the web audio oscillator, so that no external audio files are needed. I'm creating the sound of the metronome by ramping the volume of the oscillator up and down very quickly (since you can't use start() and stop() more than once), and then repeating that function at a set interval. It ends up sounding like a nice little wood block.

The code below works/sounds great in Chrome, Safari and Opera. But in Firefox, there's a nasty intermittent "click" when the volume ramps up. I've tried changing the attack/release times to get rid of the click, but they have to be really, really long before it consistently disappears. So long, in fact, that the oscillator just sounds like a sustained note.

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

Is there any way to get FF to sound like the other 3 browsers?

like image 693
schwenky Avatar asked Jan 25 '16 19:01

schwenky


2 Answers

I was getting the exact same problem in latest Opera and found the problem to be the individual sounds 'decimal time length'.

I wrote a morse code translator, and like yours, it's just a series of simple short sounds/beeps created via createOscillator.

With morse code you have a speed count (words per minute) based on a 5 letter long word like codex or paris.

To get 20 or 30 paris' per minute to finish exactly on the minute, I had to use a sound time length of, for example, 0.61. In Opera, this caused the 'end of sound click'. On changing this to 0.6 and the click disappeared across all browsers - except Firefox.

I've tried freq = 0 and gain = 0 between sounds but still get the click at the end in FF and I don't know enough about Web Audio to try anything else.

On another note, I noticed you're using a loop and timeout to get to the next tick. Have you tried an 'Oscillator onended function' instead? I've used it with a simple counter increment and variable length blank sound/note. Go to the very end of my JS if you want to have a look.

**UPDATE - I've been fiddling about with setValueAtTime() and linearRampToValueAtTime() and appeared to have cracked the click problem. Scroll to bottom of script to see example. **

(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;
}
like image 158
Griggy Avatar answered Sep 28 '22 16:09

Griggy


It would be best to get Firefox to fix the issue (if indeed it is a Firefox bug with automations). Having said that, you could probably make all the browsers be consistent by using an AudioBufferSource node that has a precomputed click waveform that you want. Just generate a sine wave, ramp it up and down as you want (manually) and play that back at regular intervals.

Not great, but it should be cross-platform.

like image 22
Raymond Toy Avatar answered Sep 28 '22 17:09

Raymond Toy