Web Audio Oscillator clicked only in Firefox

I am trying to create a simple metronome using a web sound oscillator so that no external audio files are needed. I create a metronome sound by increasing the volume of the generator up and down very quickly (since you cannot use start () and stop () more than once), and then repeat this function at a given interval. It ends up looking like a beautiful wooden block.

The code below works / sounds great in Chrome, Safari and Opera. But in Firefox there is an unpleasant intermittent "click" when the volume increases. I tried to change the attack / release time to get rid of the click, but they should really, really long before it disappears in sequence. Until then, in fact, the generator just sounds like a steady 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 a way to make FF sound like the other 3 browsers?

+6
source share
4 answers

I got the same problem in the latest Opera and found that the problem is the decimal length of individual sounds.

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

With Morse code, you have a speed score (words per minute) based on a 5-letter word, for example codex or paris.

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

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

In another note, I noticed that you are using a loop and a timeout to advance to the next tick. Have you tried to use the "Oscillator?" I used it with a simple increment counter and variable length / variable length note. Go to the very end of my JS if you want to see.

** UPDATE - I was playing with setValueAtTime () and linearRampToValueAtTime () and seemed to distort the click issue. Scroll down to the script to see an 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; } 
+2
source

It would be better if Firefox fixed the problem (if it really is a Firefox automation bug). Having said that, you could probably make all browsers consistent using the AudioBufferSource node, which has the pre-calculated click waveform you want. Simply create a sine wave, sweep it up and down as you want (manually), and play it at regular intervals.

Not really, but it should be cross-platform.

+1
source

AFAIK, this problem does not apply to Firefox, although looking at your code, I'm not sure why this does not happen in other browsers.

The problem is that the moment you plan * rampToValueAtTime for an audible source, when this source is not currently interpolated between the two ramp points, a “click” of the sound occurs, possibly due to the way the base implementation immediately starts accepting new point ramp, even if it should happen in the future.

A click sound will also be heard if you are planning a new ramp point between two points between which interpolation takes place.

What I developed as a workaround is either an alternative approach to gradually changing the AudioParam, setTargetAtTime values , or setting the value AudioParam property to the first ramp point value. Not setValueAtTime, but assignment to the value property itself before anything audible happens on this branch.


setTargetAtTime

You need neither cancelScheduledValues ​​nor setValueAtTime, just two calls to setTargetAtTime, which is just setValueAtTime with exponential interpolation with the specified length.

 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

0
source

I have the same problem, and I can’t fix it with linear or exponential ramp, and the given target once does not work either. It works fine in all other browsers, so it really annoys me. I am making a synthesizer, so it is important that it does not click. In order for the click not to be heard, it should almost disappear or disappear in half a second.

0
source

Source: https://habr.com/ru/post/1241441/


All Articles