Smoothing out values of an array
Asked Answered
F

3

20

If I had an array of numbers such as [3, 5, 0, 8, 4, 2, 6], is there a way to “smooth out” the values so they’re closer to each other and display less variance?

I’ve looked into windowing the data using something called the Gaussian function for a 1-dimensional case, which is my array, but am having trouble implementing it. This thread seems to solve exactly what I need but I don’t understand how user naschilling (second post) came up with the Gaussian matrix values.

Context: I’m working on a music waveform generator (borrowing from SoundCloud’s design) that maps the amplitude of the song at time t to a corresponding bar height. Unfortunately there’s a lot of noise, and it looks particularly ugly when the program maps a tiny amplitude which results in a sudden decrease in height. I basically want to smooth out the bar heights so they aren’t so varied.

The language I'm using is Javascript.

EDIT: Sorry, let me be more specific about "smoothing out" the values. According to the thread linked above, a user took an array

[10.00, 13.00, 7.00, 11.00, 12.00, 9.00, 6.00, 5.00]

and used a Gaussian function to map it to

[ 8.35,  9.35, 8.59,  8.98,  9.63, 7.94, 5.78, 7.32]

Notice how the numbers are much closer to each other.

EDIT 2: It worked! Thanks to user Awal Garg's algorithm, here are the results:

No smoothing Some smoothing Maximum smoothing

EDIT 3: Here's my final code in JS. I tweaked it so that the first and last elements of the array were able to find its neighbors by wrapping around the array, rather than calling itself.

var array = [10, 13, 7, 11, 12, 9, 6, 5];

function smooth(values, alpha) {
    var weighted = average(values) * alpha;
    var smoothed = [];
    for (var i in values) {
        var curr = values[i];
        var prev = smoothed[i - 1] || values[values.length - 1];
        var next = curr || values[0];
        var improved = Number(this.average([weighted, prev, curr, next]).toFixed(2));
        smoothed.push(improved);
    }
    return smoothed;
}

function average(data) {
    var sum = data.reduce(function(sum, value) {
        return sum + value;
    }, 0);
    var avg = sum / data.length;
    return avg;
}

smooth(array, 0.85);
Fagin answered 25/9, 2015 at 18:53 Comment(1)
I'm not sure I understand what you mean smooth out valuesPolad
J
11

Interesting question!

The algorithm to smooth out the values obviously could vary a lot, but here is my take:

"use strict";
var array = [10, 13, 7, 11, 12, 9, 6, 5];

function avg (v) {
  return v.reduce((a,b) => a+b, 0)/v.length;
}

function smoothOut (vector, variance) {
  var t_avg = avg(vector)*variance;
  var ret = Array(vector.length);
  for (var i = 0; i < vector.length; i++) {
    (function () {
      var prev = i>0 ? ret[i-1] : vector[i];
      var next = i<vector.length ? vector[i] : vector[i-1];
      ret[i] = avg([t_avg, avg([prev, vector[i], next])]);
    })();
  }
  return ret;
}

function display (x, y) {
  console.clear();
  console.assert(x.length === y.length);
  x.forEach((el, i) => console.log(`${el}\t\t${y[i]}`));
}

display(array, smoothOut(array, 0.85));

NOTE: It uses some ES6 features like fat-arrow functions and template strings. Firefox 35+ and Chrome 45+ should work fine. Please use the babel repl otherwise.

My method basically computes the average of all the elements in the array in advance, and uses that as a major factor to compute the new value along with the current element value, the one prior to it, and the one after it. I am also using the prior value as the one newly computed and not the one from the original array. Feel free to experiment and modify according to your needs. You can also pass in a "variance" parameter to control the difference between the elements. Lowering it will bring the elements much closer to each other since it decreases the value of the average.

A slight variation to loosen out the smoothing would be this:

"use strict";
var array = [10, 13, 7, 11, 12, 9, 6, 5];

function avg (v) {
  return v.reduce((a,b) => a+b, 0)/v.length;
}

function smoothOut (vector, variance) {
  var t_avg = avg(vector)*variance;
  var ret = Array(vector.length);
  for (var i = 0; i < vector.length; i++) {
    (function () {
      var prev = i>0 ? ret[i-1] : vector[i];
      var next = i<vector.length ? vector[i] : vector[i-1];
      ret[i] = avg([t_avg, prev, vector[i], next]);
    })();
  }
  return ret;
}

function display (x, y) {
  console.clear();
  console.assert(x.length === y.length);
  x.forEach((el, i) => console.log(`${el}\t\t${y[i]}`));
}

display(array, smoothOut(array, 0.85));

which doesn't take the averaged value as a major factor.

Feel free to experiment, hope that helps!

Jitterbug answered 25/9, 2015 at 19:44 Comment(0)
S
8

The technique you describe sounds like a 1D version of a Gaussian blur. Multiply the values of the 1D Gaussian array times the given window within the array and sum the result. For example

  1. Assuming a Gaussian array {.242, .399, .242}
  2. To calculate the new value at position n of the input array - multiply the values at n-1, n, and n+1 of the input array by those in (1) and sum the result. eg for [3, 5, 0, 8, 4, 2, 6], n = 1:

    n1 = 0.242 * 3 + 0.399 * 5 + 0.242 * 0 = 2.721

You can alter the variance of the Gaussian to increase or reduce the affect of the blur.

Sidky answered 25/9, 2015 at 19:46 Comment(4)
This is so close to what I'm looking for. How did you come up with the Gaussian array [.242, .399, .242]?Fagin
They are discrete values of the Gaussian function with (mean 0, sd = 1) at -1, 0, and 1.Sidky
Sorry, but can you be more specific as to how those can be calculated? I'm assuming you used this function. I understand where the mean and standard deviation goes; for x did you plug in -1, 0 and 1?Fagin
I know this is old, but I think you can lookup the values here: keisan.casio.com/exec/system/1180573188Mayemayeda
H
0

i stumbled upon this post having the same problem with trying to achieve smooth circular waves from fft averages. i've tried normalizing, smoothing and wildest math to spread the dynamic of an array of averages between 0 and 1. it is of course possible but the sharp increases in averaged values remain a bother that basically makes these values unfeasable for direct display.

instead i use the fft average to increase amplitude, frequency and wavelength of a separately structured clean sine. imagine a sine curve across the screen that moves right to left at a given speed(frequency) times the current average and has an amplitude of current average times whatever will then be mapped to 0,1 in order to eventually determine 'the wave's' z.

the function for calculating size, color, shift of elements or whatever visualizes 'the wave' will have to be based on distance from center and some array that holds values for each distance, e.g. a certain number of average values.

that very same array can instead be fed with values from a sine - that is influenced by the fft averages - which themselves thus need no smoothing and can remain unaltered. the effect is pleasingly clean sine waves appearing to be driven by the 'energy' of the sound.

like this - where 'rings' is an array that a distance function uses to read 'z' values of 'the wave's x,y positions.

const wave = {
          y: height / 2,
          length: 0.02,
          amplitude: 30,
          frequency: 0.5
      }
      //var increment = wave.frequency;
      var increment = 0;
      function sinewave(length,amplitude,frequency) { 
        
        ctx.strokeStyle = 'red';        
        ctx.beginPath();
        ctx.moveTo(0, height / 2);
        for (let i = 0; i < width; i+=cellSize) {
          //ctx.lineTo(i, wave.y + Math.sin(i * wave.length + increment) * wave.amplitude)
          ctx.lineTo(i, wave.y + Math.sin(i * length + increment) * amplitude);
          rings.push( map( Math.sin(i * length + increment) * amplitude,0,20,0.1,1) );
          rings.shift();
        }  
        ctx.stroke();
        increment += frequency;
      }

the function is called each frame (from draw) with the current average fft value driving the sine function like this - assuming that value is mapped to 0,1:

sinewave(0.006,averg*20,averg*0.3)

allowing fluctuating values to determine wavelength or frequency can have some visually appealing effect. however, the movement of 'the wave' will never seem natural.

i've accomplished a near enough result in my case. for making the sine appear to be driven by each 'beat' you'd need beat detection to determine the exact tempo of 'the sound' that 'the wave' is supposed to visualize.

continuous averaging of distance between larger peaks in the lower range of fft spectrum might work there with setting a semi fixed frequency - with edm...

i know, the question was about smoothing array values. forgive me for changing the subject. i just thought that the objective 'sound wave' is an interesting one that could be achieved differently. and just so this is complete here's a bit that simply draws circles for each fft and assign colour according to volume. with linewidths relative to total radius and sum of volumes this is quite nice:

//col generator
function getCol(n,m,f){
  var a             = (PIx5*n)/(3*m) + PIdiv2;
  var r             = map(sin(a),-1,1,0,255);
  var g             = map(sin(a - PIx2/3),-1,1,0,255);
  var b             = map(sin(a - PIx4/3),-1,1,0,255);
  return ("rgba(" + r + "," + g + "," + b + "," + f + ")");
}


//draw circles for each fft with linewidth and colour relative to value

function drawCircles(arr){
  var nC      = 20; //number of elem from array we want to use
  var cAv     = 0;
  var cAvsum  = 0;

  //get the sum of all values so we can map a single value with regard to this
  for(var i = 0; i< nC; i++){
    cAvsum += arr[i];
  }
  cAv         = cAvsum/nC;
  
  var lastwidth = 0;

  //draw a circle for each elem from array
  //compute linewith a fraction of width relative to value of elem vs. sum of elems
  for(var i = 0; i< nC; i++){
    ctx.beginPath();
    var radius = lastwidth;//map(arr[i]*2,0,255,0,i*300);
    //use a small col generator to assign col - map value to spectrum
    ctx.strokeStyle = getCol(map(arr[i],0,255,0,1280),1280,0.05);
    //map elem value as fraction of elem sum to linewidth/total width of outer circle
    ctx.lineWidth = map(arr[i],0,cAvsum,0,width);
    //draw
    ctx.arc(centerX, centerY, radius, 0, Math.PI*2, false); 
    ctx.stroke();
    //add current radius and linewidth to lastwidth
    var lastwidth = radius + ctx.lineWidth/2; 
  }
  
}

codepen here: https://codepen.io/sumoclub/full/QWBwzaZ always happy about suggestions.

Hayne answered 17/12, 2022 at 0:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.