Search Unity

Do people not realize how bad AudioSource.dspTime is? Can someone explain how it works?

Discussion in 'Audio & Video' started by SweetC, May 5, 2016.

  1. SweetC

    SweetC

    Joined:
    May 2, 2016
    Posts:
    4
    I posted a question here but didn't get a response.

    The manual says dspTime is ".. much more precise than the time obtained via the Time.time property." but, if I read dspTime every frame I can see that it stays the exact same value for up to 3-4 frames in a row. Even if I put this code in FixedUpdate (50 fps) it still doesn't change every frame. How can you call that precise?

    Is it this bad for everyone else?

    I noticed some small sync issues in my game and finally traced it to this. I was surprised when I couldn't find anyone else talking about it.

    my test code..
    Code (CSharp):
    1.  double lastDspTime = 0;
    2. void Update ()
    3. {
    4.      if (AudioSettings.dspTime == lastDspTime)
    5.      {
    6.          Debug.Log ("Duplicate Time: " + lastDspTime);
    7.      }
    8.      lastDspTime = AudioSettings.dspTime;
    9. }

    sample of my results..
    Code (CSharp):
    1.  ...
    2. Duplicate Time: 210.474666666667
    3. Duplicate Time: 210.474666666667
    4. Duplicate Time: 210.496
    5. Duplicate Time: 210.517333333333
    6. Duplicate Time: 210.517333333333
    7. Duplicate Time: 210.517333333333
    8. Duplicate Time: 210.538666666667
    9. Duplicate Time: 210.56
    10. Duplicate Time: 210.56
    11. ...
    Can other people test and share their results?

    I tested with Unity 4 and 5. The above results are taken at 120fps.

    Thanks
     
    olidadda likes this.
  2. michaelhartung

    michaelhartung

    Joined:
    Dec 19, 2013
    Posts:
    72
    You have to read this value in the context of the audio system using OnAudioFilterRead() which runs at the systems audio SampleRate instead of frame based. As to why Update/FixedUpdate give you the same result multiple times I can only assume that there are some race conditions affecting the dspTime in Update/FixedUpdate.
     
  3. SweetC

    SweetC

    Joined:
    May 2, 2016
    Posts:
    4
    Does it behave this way for everyone else?
     
  4. Nifflas

    Nifflas

    Joined:
    Jun 13, 2013
    Posts:
    118
    The value is the DSP time at the start of the current audio buffer. Depending on the combination of the buffer length (which seems to default to 1024 samples), the fixed timestep, and the framerate, it is completely normal for it to last longer than a single Update/FixedUpdate.

    Retrieving a current DSP time it in a non-DSP thread, then using it to schedule something on the DSP thread is not a reliable thing to do. Even if you got a more "current" value, you still couldn't do that reliably. You need to set up an absolute reference point (e.g. the DSP time you scheduled a particular sound on) that you can calculate reliable DSP times from (adding e.g. x number of beats or the length of a sound).

    This is not a problem with Unity though. Synchronizing stuff on an audio thread is just tricky stuff by nature and takes a bit of learning.
     
    Last edited: May 9, 2016
    ow3n, AndreiTache, NikH and 1 other person like this.
  5. SweetC

    SweetC

    Joined:
    May 2, 2016
    Posts:
    4
    I use playscheduled and generate all the audio start times myself so the music is all in perfect time.

    My problem is that I use those same dsp times to sync projectiles and noticed they aren't always evenly spaced. After a while I realized its because dspTime reads the same for several frames randomly.

    I should probably write a custom time method that is synced with dspTime but uses time.time so it updates every frame? Maybe?

    I'm not sure what the best solution is but thanks for the info, I appreciate it.
     
    michaelhartung likes this.
  6. Nifflas

    Nifflas

    Joined:
    Jun 13, 2013
    Posts:
    118
    That wouldn't accomplish what you wanted, you're still mistaken about the nature of the real issue. Your problem would have been present even if you'd have got more "recent" non-repeating evenly spaced DSP time values in Update/FixedUpdate. The approach to retrieve a DSP time in a non-DSP thread, and using that value without a reliable reference point to schedule a sound in the DSP thread can by nature never be sample-accurate in any software. You'll have to read up on why that is (audio buffers & passing values between parallel threads), and either learn to set up reliable reference points that you can use with your scheduling calculations, or write a custom audio filter since that code will actually run in the DSP thread.
     
    Last edited: May 9, 2016
    NikH and jerotas like this.
  7. SweetC

    SweetC

    Joined:
    May 2, 2016
    Posts:
    4
    You might misunderstand me. My music pieces all fit together very nicely and seem to be sample perfect.

    For that I read dspTime once, before the song starts, and build a whole array of all the future measure start times using addition. Then when I PlayScheduled() a peice of the music, I use a time from that array.

    My problem is that I use that same array of dspTimes Vs. current dspTime to trigger projectiles in beat. I do this inside Update(), and since dspTime doesn't update for 5 frames in a row some times, some of my projectiles fire 5 frames later than they should.

    Thanks
     
  8. Nifflas

    Nifflas

    Joined:
    Jun 13, 2013
    Posts:
    118
    Oh, okay. Sorry, I completely misunderstood the usage then! That makes this a lot easier! Declare a double that you use as a timer. On Update, perform one of these two actions. 1: Every frame AudioSettings.dspTime has a new value compared to the last frame, set your timer to that value. 2: Every frame AudioSettings.dspTime is a duplicate, you add Time.unscaledDeltaTime to your timer instead. Comparing against this timer should definitely improve the synchronization.
     
    Last edited: May 10, 2016
  9. little_box

    little_box

    Joined:
    Aug 22, 2017
    Posts:
    28
    It worked!,thank you
     
  10. Voronoi

    Voronoi

    Joined:
    Jul 2, 2012
    Posts:
    587
    I was having the same problem understanding how this worked, and @Nifflas advice really helped. If anyone else stumbles on this thread, here is a metronome based on the manual with an Event system that runs on the main thread. I believe it is sample accurate timing, if anyone sees a flaw in my logic, I would be happy to improve this code.

    Code (CSharp):
    1. // The code example shows how to implement a metronome that procedurally
    2. // generates the click sounds via the OnAudioFilterRead callback.
    3. // While the game is paused or suspended, this time will not be updated and sounds
    4. // playing will be paused. Therefore developers of music scheduling routines do not have
    5. // to do any rescheduling after the app is unpaused
    6.  
    7. using System.Collections;
    8. using System.Collections.Generic;
    9. using UnityEngine;
    10.  
    11. [RequireComponent(typeof(AudioSource))]
    12. public class Metronome : MonoBehaviour
    13. {
    14.     //Timer Events based on the beat
    15.     public delegate void Beat();
    16.     public static event Beat OnBeat;
    17.  
    18.     public delegate void DownBeat();
    19.     public static event DownBeat OnDownBeat;
    20.  
    21.     private double downBeatTime = 0;
    22.     private double lastDownBeatTime = 0;
    23.  
    24.     private double beatTime = 0;
    25.     private double lastBeatTime = 0;
    26.  
    27.     public double bpm = 140.0F;
    28.     public float gain = 0.5F;
    29.     public int signatureHi = 4;
    30.     public int signatureLo = 4;
    31.     public bool playMetronomeTick = true;
    32.  
    33.     private double nextTick = 0.0F;
    34.     private float amp = 0.0F;
    35.     private float phase = 0.0F;
    36.     private double sampleRate = 0.0F;
    37.     private int accent;
    38.     private bool running = false;
    39.  
    40.  
    41.     void Start()
    42.     {
    43.         accent = signatureHi;
    44.         double startTick = AudioSettings.dspTime;
    45.         sampleRate = AudioSettings.outputSampleRate;
    46.         nextTick = startTick * sampleRate;
    47.         running = true;
    48.     }
    49.  
    50.     private void Update()
    51.     {
    52.  
    53.         if (lastBeatTime == beatTime)
    54.         {
    55.             if (lastDownBeatTime == downBeatTime)
    56.             {
    57.                 if (OnDownBeat != null)
    58.                     OnDownBeat();
    59.             }
    60.             else
    61.             {
    62.                 if (OnBeat != null)
    63.                     OnBeat();
    64.             }
    65.         }
    66.  
    67.         downBeatTime = AudioSettings.dspTime;
    68.         beatTime = AudioSettings.dspTime;
    69.     }
    70.  
    71.     void OnAudioFilterRead(float[] data, int channels)
    72.     {
    73.         if (!running)
    74.             return;
    75.  
    76.         double samplesPerTick = sampleRate * 60.0F / bpm * 4.0F / signatureLo;
    77.         double sample = AudioSettings.dspTime * sampleRate;
    78.  
    79.         int dataLen = data.Length / channels;
    80.         int n = 0;
    81.  
    82.         while (n < dataLen)
    83.         {
    84.             float x = gain * amp * Mathf.Sin(phase);
    85.             int i = 0;
    86.             while (i < channels)
    87.             {
    88.                 data[n * channels + i] += x;
    89.                 i++;
    90.             }
    91.             while (sample + n >= nextTick)
    92.             {
    93.                 nextTick += samplesPerTick;
    94.                 if (playMetronomeTick)
    95.                     amp = 1.0F;
    96.                 if (++accent > signatureHi)
    97.                 {
    98.                     accent = 1;
    99.                     if (playMetronomeTick)
    100.                         amp *= 2.0F;
    101.                     lastDownBeatTime = AudioSettings.dspTime;
    102.  
    103.                 }
    104.  
    105.                 lastBeatTime = AudioSettings.dspTime;
    106.  
    107.                 // Debug.Log("Tick: " + accent + "/" + signatureHi);
    108.             }
    109.             if (playMetronomeTick)
    110.             {
    111.                 phase += amp * 0.3F;
    112.                 amp *= 0.993F;
    113.             }
    114.             n++;
    115.         }
    116.     }
    117. }
    Put that script on a manager in your scene and in any other script, subscribe to the events:

    Code (CSharp):
    1.  private void OnEnable()
    2.     {
    3.         Metronome.OnBeat += Beat;
    4.         Metronome.OnDownBeat += DownBeat;
    5.     }
    6.  
    7.     private void OnDisable()
    8.     {
    9.         Metronome.OnBeat -= Beat;
    10.         Metronome.OnDownBeat -= DownBeat;
    11.     }
    12.  
    13.     void Beat()
    14.     {
    15.        //Do something here
    16.     }
    17.  
    18.     void DownBeat()
    19.     {
    20.        //Do something else here
    21.     }
     
    Last edited: Feb 16, 2020
  11. olidadda

    olidadda

    Joined:
    Jul 27, 2019
    Posts:
    37
    This literally saved my bacon!
     
  12. seanwaves

    seanwaves

    Joined:
    Mar 2, 2017
    Posts:
    1
    I know it's more than a year on from when you posted this, but I read through it tonight and noticed one issue: it is possible that in low Update() framerate situations you will miss your opportunity to match the lastBeatTime and lastDownBeatTime, and you won't call the handlers.

    Could be fixed by detecting the beat and downBeat states in the dsp thread, and setting two booleans for each, if they aren't already set. In the update, fire the events if they're set, and then unset them. This will ensure you call them at least once in the update() when it gets to them.

    Alternately you could (lock and) queue them from the dsp thread, and then fire them from the update(), if you really need to track each one.
     
  13. zacharyaghaizu

    zacharyaghaizu

    Joined:
    Aug 14, 2020
    Posts:
    65
    How would this be written in code? I'm trying the method used and only getting event calls at random times
     
  14. Voronoi

    Voronoi

    Joined:
    Jul 2, 2012
    Posts:
    587
    I am by no means an audio person, and have since discovered what you observed. My code is not sample accurate and does miss beats here and there.

    As I understand it, OnAudioFilterRead is runninng on the dsp thread, so I guess you are suggesting the booleans would be there? Still, it seems like any kind of game trigger will have to be in Update, so it's still not going to appear accurate in the game. I think the only true way to have true accuracy is running everything on the dsp thread, meaning that whatever we want to show as a reaction in the game (and not as audio) will never be synchronized perfectly.
     
    april_4_short likes this.
  15. april_4_short

    april_4_short

    Joined:
    Jul 19, 2021
    Posts:
    489
    Yes, you've got it, I think.

    My observations are this:

    You can flog the game's main thread so hard the gameplay stutters to single figure frame rates, yet all logic in the OnAudioFilterRead thread (ThreadID 22 on my machine) continues without hiccup, and continues playing perfectly smooth audio - if it's not reliant on anything from the Main Thread.

    The only way to stutter this audio thread is to overload it with maths or other logic, or not provide it with sufficient amounts of the data it's looking for to make sounds smoothly.

    And dsp time seems to be incredibly reliably accurate, almost always being equidistant calls, to very tiny fractions of fractions of seconds.