Search Unity

Using LINQ to find average magnitude of float array efficiently.

Discussion in 'Scripting' started by HarryMcCaffery, Jul 15, 2018.

  1. HarryMcCaffery

    HarryMcCaffery

    Joined:
    Oct 16, 2016
    Posts:
    14
    So, in short, I have a piece of code that:

    1. Grabs x amount of floats from an audio clip,
    2. Loops through all values and applies an absolute value to them,
    3. And finds the average using Array.Average().

    This works perfectly, however it is brutally inefficient, and the vast majority of inefficiency is cause by the for loop. Is there a way, using LINQ or other, to more efficiently to get an average with all values being positive?

    I appreciate any help, I haven't really learned how to use LINQ yet.

    Also, I am grabbing almost 1000 samples per frame, however I do need to get samples from around 0.1 seconds of audio @ 16K

    Thanks.
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    LINQ isn't necessarily more efficient. It actually can be less efficient because it generates a good deal of garbage.

    I still use it personally, but only do so sparingly for things that I know the garbage isn't of high impact to the game.

    ...

    Show us your current code, we might be able to help you optimize it. Also include any rough estimates and counts you're working with if the code is not obvious as to the collection sizes.
     
    Kiwasi likes this.
  3. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    I'm not sure it would be faster, but you can do:

    Code (csharp):
    1. var positiveAverage = samples.Aggregate(x, y => x + Mathf.Abs(y)) / numberOfSamples;
    That will get you the average and loop only once through the list. You'd need to know numberOfSamples, which I'd think you'd know since you're getting the samples first, but if not you can do "var numberOfSamples = samples.Length" if it's a list or "var numberOfSamples = samples.Count()" if it's not.
     
  4. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    One thing I would seriously consider is using some statistics to cut down the number of samples you need. You can get pretty close approximations to the average of a large collection by taking a random set of samples. This is especially true for a real world data which frequently has an easily modelable distribution.

    Unless you are doing extremely precise work, there is no real benefit to taking the average of 1000 samples over taking an average of 20 random samples.

    (Also you should build your average number inside of the loop, instead of doing a separate loop afterwards with Array.Average. But honestly that won't make a huge amount of difference.)
     
  5. HarryMcCaffery

    HarryMcCaffery

    Joined:
    Oct 16, 2016
    Posts:
    14
    Awesome! I appreciate all of the responses. For those who were interested, here is what my code looked like:
    Code (CSharp):
    1.  
    2. float[] aryOfSamples = new float[(int)(source.clip.samples s.clip.length * integralTime)];
    3. source.clip.GetData(aryOfSamples, ((int)(source.clip.samples /source.clip.length * source.time)));
    4.                   for (int i = 0; i < aryOfSamples.Length; i++)
    5.                        aryOfSamples[i] = Mathf.Abs(aryOfSamples[i]);
    6. int averageVal = aryOfSamples.Average();
    Kiwasi is totally right, I don't know why I didn't think of using a random sample myself- that would definitely be the easiest way to cut down sample size and thus increase efficiency. Thanks for all the help.
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    First off you should probably cache that array from the get go instead of creating a new array every frame (if you're doing this in Update that is). Creating a new large array frequently is expensive.

    Next you technically loop the array twice by first doing the abs, and then averaging. Why not do that in the same step?

    (sorry I changed your name of 'aryOfSamples' to just 'samples' because my god is that a butt ugly name for a var)
    Code (csharp):
    1.  
    2. float average = 0f;
    3. for(int i = 0; i < samples.Length; i++)
    4. {
    5.     average += Mathf.Abs(samples[i]);
    6. }
    7. average /= samples.Length;
    8.  
     
    Kiwasi likes this.
  7. HarryMcCaffery

    HarryMcCaffery

    Joined:
    Oct 16, 2016
    Posts:
    14
    All good on the name change lol- good advice on condensing it into one loop, that will definitely make it more efficient.

    I can't cache the array however, because the audioclip is changing constantly (It is the output from Voice Chat)
     
  8. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    You can cache the array by making it a private variable in the class:
    Code (csharp):
    1. private float[] samples = new float[MAX_SAMPLES];
    And then just replace the data in the array each time through the loop without having to make a new array:
    Code (csharp):
    1. source.clip.GetData(samples, offset);
     
    Kiwasi likes this.
  9. HarryMcCaffery

    HarryMcCaffery

    Joined:
    Oct 16, 2016
    Posts:
    14
    Oh yeah that would work, I suppose the amount of samples I need to get would not change. Good call, and thanks for all the help.


    Oh yeah, and that bit of code actually isn't too bad of a way to sync a character's lips with speech at run time.