How can you calculate latency from Mic to speaker?

I am experimenting with some settings to get real time audio from the mic. I am to the point that I can not really hear if the latency is getting better or worse. Is there a way to calculate the latency between the mic input and the speaker output so I can display it on the screen?

Thanks in advance, I plan on sharing my simple real time audio script once I get it tuned.

Ok, I think I figured it out. This script plays back mic input in real time and adjusts latency setting for the lowest amount.
If you see any improvements please let me know.

To use this script you need a button, text, and dropdown. The script will wire them up for you and adjust certain audio settings. Put the script on an empty object with an audio source. You should be good to go.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(AudioSource))]
public class Audio : MonoBehaviour
{
    #region Elements
    static int sampleRate = 0;
    public enum Dsp_Buffer_Sizes { b_128 = 128, b_256 = 256, b_340 = 340, b_480 = 480, b_512 = 512, b_1024 = 1024 };

    public Text TextStatus;
    public Dropdown DropDownMicrophone;
    public Button ButtonRecord;

    [Tooltip("Seconds of input that will be buffered. Too short = lost data")]
    public int micRecordLength = 1;
    [Tooltip("Seconds until mic input is played through audio source. Too short = artifacts")]
    public float latencyBuffer = 0.001f;
    [Tooltip("Larger buffer size adds latency. Too short = artifacts")]
    public Dsp_Buffer_Sizes Dsp_Buffer_Size = Dsp_Buffer_Sizes.b_256; //This the the "Best Latency" value

    private Text ButtonText;
    private AudioSource audioSource;
    private List<string> ListDevice;
    private int selectedDevice = 0;
    #endregion

    /// <summary>Attempts to adjust settings for less latency</summary>
    void SetupAudioSource()
    {
        audioSource = this.GetComponent<AudioSource>();
        //bypass to try and speed things up
        audioSource.bypassEffects = true;
        audioSource.bypassListenerEffects = true;
        audioSource.bypassReverbZones = true;
        audioSource.priority = 0;
        audioSource.pitch = 1;
    }
    /// <summary>Attempts to adjust settings for less latency</summary>
    void SetupGlobalAudio()
    {
        AudioConfiguration config = AudioSettings.GetConfiguration();
        config.dspBufferSize = (int)Dsp_Buffer_Size;
        config.sampleRate = 0;          //11025, 22050, 44100, 48000, 88200, 96000,
        config.numVirtualVoices = 1;    //1, 2, 4, 8, 16, 32, 50, 64, 100, 128, 256, 512,
        config.numRealVoices = 1;       //1, 2, 4, 8, 16, 32, 50, 64, 100, 128, 256, 512,
        config.speakerMode = AudioSpeakerMode.Mono;
        AudioSettings.Reset(config);
    }

    #region DropDown and Button Functions
    private void DropDown_Changed(int index)
    {
        selectedDevice = index;
    }

    private void Button_Click()
    {
        if (IsRecording)
        {
            IsRecording = false;
            ButtonText.text = "Record";
        }
        else
        {
            Record();
        }
    }
    #endregion

    /// <summary>Set = Start / stop mic recording. Get = Microphone.IsRecording</summary>
    private bool IsRecording
    {
        get { return Microphone.IsRecording(ListDevice[selectedDevice]); }
        set
        {
            if (value) //true
            {
                if (!Microphone.IsRecording(ListDevice[selectedDevice]))
                {
                    sampleRate = AudioSettings.outputSampleRate;
                    //Start recording and store the audio captured from the microphone at the AudioClip in the AudioSource 
                    audioSource.clip = Microphone.Start(ListDevice[selectedDevice],true , micRecordLength, sampleRate );// maxFreq);
                    if (!(RecordHeadPosition > 0)) { }          //wait for mic ready
                    audioSource.loop = true;                    //continual output
                    audioSource.mute = false;                   //Hack for bug
                    audioSource.PlayDelayed(latencyBuffer);     //must be delayed or bad results
                }
            }
            else //false
            {
                if (Microphone.IsRecording(ListDevice[selectedDevice]))
                {
                    Microphone.End(ListDevice[selectedDevice]);
                    audioSource.clip = null;
                    audioSource.loop = false;
                }
            }
        }
    }

    /// <summary>Loads recording device names into DropDown</summary>
    /// <returns>status string</returns>
    private string LoadMicrophoneDevices()
    {
        if (Microphone.devices.Length <= 0)
            return ("Microphone not connected!");

        DropDownMicrophone.ClearOptions();
        ListDevice = new List<string>();

        foreach (string device in Microphone.devices)
            ListDevice.Add(device);

        DropDownMicrophone.AddOptions(ListDevice);
        return "Recording devices loaded";
    }

    private int RecordHeadPosition
    {
        get { return Microphone.GetPosition(ListDevice[selectedDevice]); }
    }

    private void Record()
    {
        if (!IsRecording)
        {
            IsRecording = true;
            ButtonText.text = "Stop";
            TextStatus.text = "Recording";
        }
        else
        {
            TextStatus.text = "Device is already recording...";
        }
    }

    private void Start()
    {
        SetupGlobalAudio();
        SetupAudioSource();
        //setup listeners in code to make less confusing later on
        ButtonRecord.onClick.AddListener(Button_Click);
        DropDownMicrophone.onValueChanged.AddListener(DropDown_Changed);

        ButtonText = ButtonRecord.GetComponentInChildren<Text>();
        TextStatus.text = LoadMicrophoneDevices();
    }

    #region Everything below is for calculating and auto setting latency
    static int Read_Position = 0;
    static int Max_Latency = 0;
    static int old_Write_Position = 0;
    static bool Reload = false;

    private void FixedUpdate()
    {
        if (IsRecording)
        {
            float latencySeconds = (float)Max_Latency / (float)AudioSettings.outputSampleRate;
            TextStatus.text = Max_Latency.ToString() + " Samples / " + AudioSettings.outputSampleRate.ToString() + " samplerate = " + latencySeconds.ToString() + " seconds Latency";

            //auto select best latency value
            if (Max_Latency > latencyBuffer * sampleRate)
            {
                latencyBuffer = ((float)Max_Latency / (float)sampleRate) + 0.01f;
                IsRecording = false;
                IsRecording = true;
                Reload = true;
            }
        }
    }

    private void OnAudioFilterRead(float[] data, int channels)
    {
        if (Reload)
        {
            Max_Latency = 0;
            Read_Position = 0;
            old_Write_Position = 0;
            Reload = false;
        }
        Read_Position += data.Length;
        int Write_Position = Microphone.GetPosition(ListDevice[selectedDevice]);
       
        if (Write_Position < old_Write_Position)    //Check if write buffer looped
        {
            Read_Position = data.Length;
        }
        //latency in samples
        int Latency = Read_Position - Write_Position;
        if (Latency > Max_Latency)
            Max_Latency = Latency;

        old_Write_Position = Write_Position;
    }
    #endregion
}
1 Like

Thanks a bunch! It’s very nice from you to share your code.

It did improve the latency a LOT compared to the original method:

m_audio.clip = Microphone.Start(device, true, 1, 4400);
m_audio.loop = true;
 while (!(Microphone.GetPosition(device) > 0)) { }