Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How to make most accurate metronome in Unity?

Discussion in 'Scripting' started by Nadan, Oct 23, 2014.

  1. Nadan

    Nadan

    Joined:
    Jan 20, 2013
    Posts:
    341
    Hi! I'm making a simple metronome in Unity and have some problems with it. At first I tried this:

    Code (javascript):
    1. function Start ()
    2. {
    3.     bpm = 140;
    4.     bpmInSeconds = 60 / bpm;
    5.     metronome = true;
    6.     nextTime = Time.time;
    7.     StartMetronome();
    8. }
    9.  
    10. function StartMetronome()
    11. {
    12.   while(metronome)
    13.   {
    14.     Debug.Log("Tick");
    15.     audio.Play();
    16.     nextTime += bpmInSeconds;
    17.     yield WaitForSeconds(nextTime - Time.time);
    18.    }
    19. }
    However it's not solid.

    In Unity docs there is this AudioSettings.dspTime.

    http://docs.unity3d.com/ScriptReference/AudioSettings-dspTime.html

    Code (javascript):
    1. // The code example shows how to implement a metronome that procedurally generates the click sounds via the OnAudioFilterRead callback.
    2. // While the game is paused or the suspended, this time will not be updated and sounds playing will be paused. Therefore developers of music scheduling routines do not have to do any rescheduling after the app is unpaused
    3.  
    4. @script RequireComponent(AudioSource)
    5.  
    6. public var bpm : double = 140.0;
    7. public var gain : float = 0.5f;
    8. public var signatureHi : int = 4;
    9. public var signatureLo : int = 4;
    10.  
    11. private var nextTick : double = 0.0;
    12. private var amp : float = 0.0f;
    13. private var phase : float = 0.0f;
    14. private var sampleRate : double = 0.0;
    15. private var accent : int;
    16. private var running : boolean = false;
    17.  
    18. function Start ()
    19. {
    20.   accent = signatureHi;
    21.   var startTick = AudioSettings.dspTime;
    22.   sampleRate = AudioSettings.outputSampleRate;
    23.   nextTick = startTick * sampleRate;
    24.   running = true;
    25. }
    26.  
    27. function OnAudioFilterRead(data:float[], channels:int)
    28. {
    29.   if(!running)
    30.   return;
    31.   var samplesPerTick = sampleRate * (60.0f / bpm) * (4.0 / signatureLo);
    32.   var sample = AudioSettings.dspTime * sampleRate;
    33.   var dataLen = data.length / channels;
    34.   for(var n = 0; n < dataLen; n++)
    35.   {
    36.   var x : float = gain * amp * Mathf.Sin(phase);
    37.   for(var i = 0; i < channels; i++)
    38.   data[n * channels + i] += x;
    39.   while (sample + n >= nextTick)
    40.   {
    41.   nextTick += samplesPerTick;
    42.   amp = 1.0;
    43.   if(++accent > signatureHi)
    44.   {
    45.   accent = 1;
    46.   amp *= 2.0;
    47.   }
    48.   Debug.Log("Tick: " + accent + "/" + signatureHi);
    49.   }
    50.   phase += amp * 0.3;
    51.   amp *= 0.993;
    52.   }
    53. }
    54.  
    This was working great! But...

    Then I tried to add code that I need in my app after the actual "tick".

    Debug.Log("Tick: " + accent + "/" + signatureHi);

    If I put in: audio.Play();

    Code (javascript):
    1.   }
    2.   Debug.Log("Tick: " + accent + "/" + signatureHi);
    3.   audio.Play();
    4.   }
    5. ...
    6.  
    Or try to change a sprite with: GetComponent(SpriteRenderer).sprite = music[4];

    I get an error:

    get_audio can only be called from the main thread.
    Constructors and field initializers will be executed from the loading thread when loading a scene.
    Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.


    So my question is. Is there a way to make sprite change, audio played etc with this AudioSettings.dspTime script? I quess the AudioSettings.dspTime and other functions are not "in the same time" or something and therefore can't be called?

    Or is there a better way to do a metronome in Unity?
     
    Last edited: Oct 23, 2014
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,840
    Yep... from the OnAudioFilterRead docs:
    But, you could just check AudioSettings.dspTime from your Update method, or you could set a property on your MonoBehavior from OnAudioFilterRead, and then check that in Update to do whatever else you need to do.
     
  3. Nadan

    Nadan

    Joined:
    Jan 20, 2013
    Posts:
    341
    Hi JoeStrout, thank you for replying.

    I'm not sure if I understood you correctly but I tried to use AudioSettings.dspTime instead of Time.time inside Update:

    Code (javascript):
    1. function Start ()
    2. {
    3.   bpm = 200;
    4.   bpmInSeconds = 60 / bpm;
    5.   nextTime = AudioSettings.dspTime + bpmInSeconds;
    6. }
    7.  
    8. function Update ()
    9. {
    10.    if (AudioSettings.dspTime >= nextTime)
    11.    {
    12.      Debug.Log("Tick");
    13.      audio.Play();
    14.      nextTime += bpmInSeconds;
    15.    }
    16. }
    With this code it still seems to have problems when the tempo is higher, like 200 or above. Is this because Update is not fast enough?
     
  4. mesayre

    mesayre

    Joined:
    Dec 10, 2014
    Posts:
    1
    Nadan, I would say that yes, you're probably bumping up against the limits of Update with high tempos, and I'd be surprised if there weren't some variation at lower tempos, albeit less noticeable by ear. Have you looked into AudioSource.PlayScheduled() ?

    Using that, you can schedule a a clip to play at an exact dspTime in the future, and that's the way they recommend to do things like switching audio clips with no gaps. There's some code in the docs: http://docs.unity3d.com/ScriptReference/AudioSource.PlayScheduled.html

    Hope it helps!
     
    JoeStrout likes this.
  5. Klarax

    Klarax

    Joined:
    Dec 2, 2013
    Posts:
    17
    i would use FixeUpdate

    and Time.fixedDeltaTime

    this would ensure that 1 second can pass no matter what the framerate is/will be.
     
  6. copertino1984

    copertino1984

    Joined:
    Dec 6, 2018
    Posts:
    1
    I have your same problem.
    but then you solved it?