Search code examples
javascriptloopsaudiosynthesizer

How to fast change frequency of played tone without crackling?


I have loop at 50-200FPS and want to visualise data with sound. I need fast and smooth respond to data changing, like Therminvox. What library and approach is best for this?

Issues with simpleTones.js:

  • sound degrade over ~2min and finally stopped working
  • i don't find how to change frequency, but not only play separate notes

Issues with Tone.js:

  • i don't find how to change frequency

document.onmousemove=init
bInit=false;
Timeout=100;
NoteLength=100;
function init() //this is not solving alert "The AudioContext was not allowed to start"
{
    if(bInit) return;
    //const osc = new Tone.Oscillator(440, "sine").toDestination().start();
    setTimeout(loop, 200);
    bInit=true;
}
function freqRand(){return 50+Math.random()*1000;}

function loop( )
{
    playTone(freqRand(),'sine',NoteLength/1000)
    setTimeout(loop, Timeout);
}


//--------UI
function sliderTimeout_change(v){
    Timeout=v; document.getElementById('Timeout').innerText=v;
}
function sliderNoteLength_change(v){
    NoteLength=v; document.getElementById('NoteLength').innerText=v;
}
<script src="https://cdn.jsdelivr.net/gh/escottalexander/simpleTones.js/simpleTones.js"></script>
<!-- <script src="Tone.js"></script>  -->
<body>
Timeout = <i id='Timeout'></i><br>
<input id='sliderTimeout' type="range" min="10" max="200" value="100" oninput="sliderTimeout_change(this.value)"><br><br>
NoteLength = <i id='NoteLength'></i><br>
<input id='sliderNoteLength' type="range" min="10" max="4000" value="100" oninput="sliderNoteLength_change(this.value)">
</body>


Solution

  • Task solved with next code, using builtin linearRampToValueAtTime but it is better to have addition exaple of how to do this with Buffer.

    document.onclick=init //onmousemove is not solving alert "The AudioContext was not allowed to start"
    bInit=false;
    Timeout=100;
    var osc;
    var aContext;
    function init()
    {
        if(bInit) return;
        
        aContext = new(window.AudioContext || window.webkitAudioContext)();
        osc = aContext.createOscillator(); 
        osc.type = 'sine';  //sine square sawtooth triangle 
        var now = aContext.currentTime;
        osc.frequency.setValueAtTime(440, now);
        osc.connect(aContext.destination); 
        osc.frequency.setValueAtTime(440, now+1);
        osc.start(now);
        
        setTimeout(loop, 100);
    
        bInit=true;
    }
    function loop( )
    {
        let freq=freqRand(); freq=smothFrequency(freq);
        
        osc.frequency.linearRampToValueAtTime(freq,aContext.currentTime+Timeout/1000)
        
        setTimeout(loop, Timeout);
    }
    
    function freqRand(){return 300+Math.random()*300+ Math.sin(aContext.currentTime/10)*250;}
    
    //============================== end of main part, next is for playing
    //--------signal conditioning, smooth
    var freq_avg=0;
    var freq_avg_k=0.1;
    var freq_RangeMul=1;
    
    function smothFrequency(freq_new)
    {
        freq_new*=freq_RangeMul;
        if(freq_avg==0 || bNoSmooth) freq_avg=freq_new;
        else //smooth freq change
        {
            if(Math.abs(freq_avg-freq_new)<Timeout/4) // no smooth when changes 4hz/ms
                    freq_avg=freq_avg;
            else 
            {
                if(Timeout>50) //less smooth for long timeout, or 'sharp'
                {
                    freq_avg=freq_avg*(1-freq_avg_k*4)+freq_new*freq_avg_k*4;
                    if(freq_avg_k>=0.25)
                    {
                        if(bSharpKeepRange)
                        {
                            if(freq_avg<0) freq_avg=-freq_avg;
                            if(freq_avg>1600) freq_RangeMul/=1.05
                            else if(freq_avg<40) freq_RangeMul*=1.05
                        }
                    }
                }
                else
                    freq_avg=freq_avg*(1-freq_avg_k)+freq_new*freq_avg_k; 
            }
        }
        //console.log(aContext.currentTime, freq, freq_RangeMul);
        return freq_avg;
    }
    
    //--------UI
    var cc=null
    function sliderTimeout_change(v){
        cc=v;
        v=v.value
        Timeout=v; document.getElementById('Timeout').innerText=v;
    }
    function sliderNoteLength_change(v){
        NoteLength=v; document.getElementById('NoteLength').innerText=v;
    }
    bNoSmooth=false;
    function sliderfreq_avg_k_change(v){
        freq_avg_k=v/1000; 
        bNoSmooth=(freq_avg_k>0.22 && freq_avg_k<0.33);
        let str=freq_avg_k;
        if(bNoSmooth)str='no smooth';
        if(freq_avg_k>0.33)
        {
            document.getElementById('bSharpKeepRange').style.display='block';
            str=str+' sharp';
        }
        else document.getElementById('bSharpKeepRange').style.display='none';
        document.getElementById('freq_avg_k').innerText=str;
    }
    function checkbox_bSharpKeepRange_click(checkbox)
    {
      bSharpKeepRange=checkbox.checked
       freq_avg=100;
       freq_RangeMul=1;
    }
    bSharpKeepRange=false;
    <body>
    Timeout ms = <i id='Timeout'></i><br>
    <input id='sliderTimeout' type="range" min="10" max="1000" value="10" oninput="sliderTimeout_change(this)"><br><br>
    freq change smooth factor = <i id='freq_avg_k'></i><br>
    <input id='sliderfreq_avg_k' type="range" min="2" max="550" value="10" oninput="sliderfreq_avg_k_change(this.value)"><br>
    <i id="bSharpKeepRange">
    keep range 
     <input id='checkbox_bSharpKeepRange' type="checkbox" onclick='checkbox_bSharpKeepRange_click(this);'>
    </i>
    </body>