oscilloscope style – loop getFloatTimeDomainData(timeData) in a circle

I’m doing simple audio visualisations in canvas for fun and found getFloatTimeDomainData(timeData) to deliver the nicest results, cause it is so naturally irregular. One visualisation attempt includes a horizontal wave taking timeData, fixed at both ends by a sine overlay. This works nicely; string vibrates in the middle only.

My attempt to present that same data in circular form must fail, due to the time based nature and float data. However, i’ve seen oscilloscopes do just that and my question is: how would i have to treat timeData. It looks nice as is even with the break in curve, hardly noticeable but i want to know how i could do it. Anyone an idea?

index.html:

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>Sound in vis</title>
  <style type="text/css">
  body {background-color:#000000; color:#333333;
  overflow: hidden;} 
  
  canvas {
    background-color:#000000; 
    position: absolute;   
    display: block;
     height: 100%;
     width: 100%;
     top:0;
     left:0;
     //filter: blur(1px); 
     }
     #fps {
      color:#F3F3F3;
     }
</style>
</head>
<body>
<canvas id="myCanvas"></canvas>
<script src="ImprovedNoise.js"></script>
<script src="index_version.js"></script>
</body>
</html>
ImprovedNoise.js
// http://mrl.nyu.edu/~perlin/noise/

var ImprovedNoise = function () {

    var p = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,
         23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,
         174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,
         133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,
         89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,
         202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,
         248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,
         178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,
         14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,
         93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180];

    for (var i=0; i < 256 ; i++) {

        p[256+i] = p[i];

    }

    function fade(t) {

        return t * t * t * (t * (t * 6 - 15) + 10);

    }

    function lerp(t, a, b) {

        return a + t * (b - a);

    }

    function grad(hash, x, y, z) {

        var h = hash & 15;
        var u = h < 8 ? x : y, v = h < 4 ? y : h == 12 || h == 14 ? x : z;
        return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);

    }

    return {

        noise: function (x, y, z) {

            var floorX = ~~x, floorY = ~~y, floorZ = ~~z;

            var X = floorX & 255, Y = floorY & 255, Z = floorZ & 255;

            x -= floorX;
            y -= floorY;
            z -= floorZ;

            var xMinus1 = x -1, yMinus1 = y - 1, zMinus1 = z - 1;

            var u = fade(x), v = fade(y), w = fade(z);

            var A = p[X]+Y, AA = p[A]+Z, AB = p[A+1]+Z, B = p[X+1]+Y, BA = p[B]+Z, BB = p[B+1]+Z;

            return lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z), 
                            grad(p[BA], xMinus1, y, z)),
                        lerp(u, grad(p[AB], x, yMinus1, z),
                            grad(p[BB], xMinus1, yMinus1, z))),
                    lerp(v, lerp(u, grad(p[AA+1], x, y, zMinus1),
                            grad(p[BA+1], xMinus1, y, z-1)),
                        lerp(u, grad(p[AB+1], x, yMinus1, zMinus1),
                            grad(p[BB+1], xMinus1, yMinus1, zMinus1))));

        }
    }
}
index_version.js
const myCanvas = document.getElementById("myCanvas");
const ctx = myCanvas.getContext("2d");


var W           = myCanvas.width = window.innerWidth;
var Wcenter = W/2;
var H           = myCanvas.height = window.innerHeight;
var Hcenter = H/2;
var bpmAdd  = H/40;
var mathPI  = Math.PI;
var mathPIhalf = Math.PI/2;
var mathPI2 = Math.PI*2;
var bstate  = true; //toggle bool stop start animation
var fps, fpsInterval, startTime, now, then, elapsed;
fps = 30;
fpsInterval = 1000 / fps;

then = window.performance.now();
startTime = then;

let freqs;
let timeData;

var bumpcount=0;

function round(int){return Math.round(int);}
function map(value, istart, istop, ostart, ostop){
  return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
}
function percOfRange(value, range, maxval){
    return((value<range) ? Math.round(value*100/range)/100 : maxval);
}
function sin(e){return Math.sin(e);}
function cos(e){return Math.cos(e);}

function degrees_to_radians(degrees){  return degrees * Math.PI;}
function radians_to_degrees(radians){  return radians * 180 / Math.PI;};

function ease(p, g){
  if (p < 0.5)
    return 0.5 * Math.pow(2*p, g);
  else
    return 1 - 0.5 * Math.pow(2*(1 - p), g);} 

let elapsedAV       = [];
var elapsedMean = 0;
var noisePos = 0;
var perlin = new ImprovedNoise();

navigator.mediaDevices.enumerateDevices().then(devices => {
  devices.forEach((d, i) => console.log(d.kind, d.label, i));
  navigator.mediaDevices
    .getUserMedia({ audio: {deviceId: devices[2].deviceId}})
    .then(stream => {
      const context = new (window.AudioContext || window.webkitAudioContext)();
      const analyser = context.createAnalyser();
      analyser.maxDecibels = -2;
      analyser.smoothingTimeConstant = 0.6;

      const source = context.createMediaStreamSource(stream);
      source.connect(analyser);
      analyser.connect(context.destination);
      analyser.fftSize = 1024;

      const bufferlength = analyser.frequencyBinCount;
      freqs = new Uint8Array(bufferlength);
      const tDataLength = 128;
      //timeData = new Uint8Array(bufferlength);
      timeData = new Float32Array(bufferlength);
      var cons = 0;
      var colcount = 0;


      function countcol(inc, max){
        (colcount>max) ? colcount = 0 : colcount +=inc;
        return(colcount);
      }      
                 
      let bars                          = 11;
            let bump                            = 0;
            
      function draw(){
        
        analyser.getByteFrequencyData(freqs);
        //analyser.getByteTimeDomainData(timeData);
        analyser.getFloatTimeDomainData(timeData);
        //analyser.getByteTimeDomainData(freqs);
        const maxvalT                   = Math.max(...timeData);
            const minvalT                   = Math.min(...timeData);
            //console.log(maxvalT + ", " + minvalT);
       
        var average                 = 0;        
        var fLength                 = analyser.fftSize/2; //128,256,512,1024
        

        // Draw bars
        const deltabar              = round(fLength / bars);
        const maxval                    = Math.max(...freqs);
        const minval                    = Math.min(...freqs);
        const indexmax          = freqs.indexOf(maxval);
        const indexmin          = freqs.indexOf(minval);
        const range                 = maxval - minval;
        const rangefactor   = 360 / range * 0.1;
        const radians           = mathPI2 / bars;// * rangefactor;        
        var freqsum                 = 0;
        var freqsumAverage  = 0;
        var fcount                  = 0;
        var fval                        = 1;
        var colArr                      = [];

        for (var i=1; i<5; i++){
          fval = freqs[i];          
          fcount ++;
          freqsum+=fval;
        }
        
        freqsumAverage = round(map( freqsum / fcount,0,256,0,1)*1000)/1000;
        //console.log(freqsumAverage + ", elapsed: " + round(elapsed));

        for (var c=fcount; c<fLength;c++){
            fval = freqs[c];
          freqsum+=fval;
        }        
        let median = round(freqsum / fLength);
                average     = map(median,0,128,0,1);                
        noisePos += 0.01+(average/1000);
        var noiseVal = perlin.noise(noisePos, 0, 0)+0.5;//Math.abs()
        colcount    = round(map(noiseVal,0,1,0,360))%360;
                //console.log(colcount);

          let hue         = colcount;
          let saturation    = (freqsumAverage*30)+70;
          let lum               = (freqsumAverage*20)+30;      
          let alpha             = (freqsumAverage*0.5)+0.5;

                    colArr[0]      = "hsla(" +  hue %360 + ", " + saturation  + "% ," + lum + "%, " + (alpha) + ")";
          colArr[1]      = "hsla(" +  (hue+60 %360) + ", " + saturation  + "% ," + lum + "%, " + (alpha) + ")";
          colArr[2]      = "hsla(" +  (hue+120 %360) + ", " + saturation  + "% ," + lum + "%, " + (alpha) + ")";
          colArr[3]      = "hsla(" +  (hue+180 %360) + ", " + saturation  + "% ," + lum + "%, " + (alpha) + ")";
          colArr[4]      = "hsla(" +  (hue+240 %360) + ", " + saturation  + "% ," + lum + "%, " + (alpha) + ")";

                
        //////////////////////////////////////////////////////////////////////////
        //timedata vis - oscillo style

                //************************************************************************
            //make timedata a circle    
            
            //var cbars     = round(map(sin(now)*freqsumAverage,-1,1,0,bufferlength*freqsumAverage));
            var cbars       = bufferlength;
            let slice   = mathPI2 / cbars;
            var segm        = round(bufferlength/cbars);
                let angle   = slice;
                
                ctx.lineCap = "round";
                var Csize   = 10;

                var curveStartx,curveStarty,vstart; 
                
                ctx.strokeStyle=colArr[3];
                ctx.beginPath();
                
            for(var i = 0; i <= cbars; i++){                    

                var thistimedata = timeData[segm*i];
                    const v = map(thistimedata,-1,1,0,Csize);               
                var cx  = cos(angle)*60;
                var cy  = sin(angle)*60;
                
                var cx2 = cx*v+Wcenter;
                var cy2 = cy*v+Hcenter;

                ctx.lineWidth = v%4;                
                ctx.lineTo(cx2, cy2);
                    
                if(i==0){ curveStartx = cx2/v;curveStarty = cy2/v;vstart=v;}
                angle += slice;
                }
                ctx.lineTo(curveStartx*vstart, curveStarty*vstart);
                ctx.stroke();
                

        //************************************************************************
            //make timedata a horizontal wave - fixed at both ends

                ctx.strokeStyle = colArr[0];
            ctx.beginPath();
            var datarange = bufferlength/2;
            const sliceWidth = W / datarange;
            let x = 0;
            
            //increment to limit signal to 0 at screen borders
                var incr = mathPIhalf/(datarange/2);

                timeData.forEach((data, i) => {

                    //multiply sin of predefined increment with data so as to limit ends of array to 0
                const v = map(sin(incr*i)*data,-1,1,0,1);
                const y = v*H;
                ctx.lineWidth = v*2; 
                if (i === 0) {
                ctx.moveTo(x, y);
                } else {
            ctx.lineTo(x, y);
                }
                x += sliceWidth;
            });             
            ctx.stroke();

            //************************************************************************
            //end timedata to horizontal string
            
      }
            //////////////////////////////////////////////////////////////////////////
      ///end function draw()

      function loop(){
        if(bstate === true ){

            var clearCounter = 0;
            now = performance.now();
            elapsed = round(now - then);
            elapsedAV.push(elapsed);
            
            if( sin(now)>0.5){              
                clear();
            }

            if(elapsedAV.length > 32){              
                        elapsedAV.shift();
                        elapsedMean = round(elapsedAV.reduce((a,b) => a + b)/elapsedAV.length);                     
                        //fps = elapsedMean;
            } 
                    //console.log(elapsed + ", " + elapsedMean);

            if (elapsed >= fpsInterval) {
                
                        //fpsInterval = 1000 / fps;                     
                        bpmAdd  = H/elapsedMean;
            draw();
            then = now - (elapsed % fpsInterval);
            }
            myReq = requestAnimationFrame(loop);
        }
        
      }     
      
      function clear(){
                        ctx.globalCompositeOperation = 'destination-out';
                ctx.fillStyle = "hsla(0, 0% , 0%, 0.1)";
                ctx.fillRect(0, 0, W, H);
                ctx.globalCompositeOperation = 'lighter';               
      }

      function toggle(){
        cancelAnimationFrame(myReq);
        bstate = !bstate;
        myReq = requestAnimationFrame(loop);       
      } 

      window.addEventListener( 'resize',  function(){
            cancelAnimationFrame(myReq);
          W = myCanvas.width = window.innerWidth;
          H = myCanvas.height = window.innerHeight;
          myReq = requestAnimationFrame(loop);
      });

      window.addEventListener( 'click', toggle);
      myReq = requestAnimationFrame(loop);
   });
});

..route sound to browser via virtual cable and allow window to use ‘microphone’.

The question concerns the presented visualisation of timeData in circular form i.e. wrapped around a circle. The result can not be a closed curve as starting point and end point of timedata array don’t match.

Desired result is a closed loop representation aka oscilloscope that displays xy circle continuous. In the example the ends are just connected, which results in a horizontal line there or a visible