Skip to content

Make TimeSync a Unity Singleton #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 63 additions & 38 deletions Runtime/Scripts/BaseOutlet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ public abstract class ABaseOutlet<TData> : MonoBehaviour
private bool UniqueFromInstanceId = true;

public abstract List<string> ChannelNames { get; }
public virtual int ChannelCount { get { return ChannelNames.Count; } }

public virtual int ChannelCount
{
get { return ChannelNames.Count; }
}

public abstract channel_format_t Format { get; }

protected StreamOutlet outlet;
protected TData[] sample;
// A singleton of a TimeSync object can ensure that all pushes that happen on the same moment (update/fixed/late etc)
// on the same frame will have the same timestamp.
private TimeSync timeSync;

// Add an XML element for each channel. The automatic version adds only channel labels. Override to add unit, location, etc.
protected virtual void FillChannelsHeader(XMLElement channels)
Expand All @@ -36,8 +38,6 @@ protected virtual void FillChannelsHeader(XMLElement channels)
protected virtual void Start()
{
// TODO: Check StreamName, StreamType, and moment are not null.

timeSync = gameObject.GetComponent<TimeSync>();
sample = new TData[ChannelCount];

var hash = new Hash128();
Expand All @@ -49,59 +49,60 @@ protected virtual void Start()
ExtendHash(hash);

double dataRate = IrregularRate ? LSL.LSL.IRREGULAR_RATE : LSLCommon.GetSamplingRateFor(moment);
StreamInfo streamInfo = new StreamInfo(StreamName, StreamType, ChannelCount, dataRate, Format, hash.ToString());
StreamInfo streamInfo =
new StreamInfo(StreamName, StreamType, ChannelCount, dataRate, Format, hash.ToString());

// Build XML header. See xdf wiki for recommendations: https://github.com/sccn/xdf/wiki/Meta-Data
XMLElement acq_el = streamInfo.desc().append_child("acquisition");
acq_el.append_child_value("manufacturer", "LSL4Unity");
XMLElement channels = streamInfo.desc().append_child("channels");
FillChannelsHeader(channels);

outlet = new StreamOutlet(streamInfo);
}

// Override to extend hash
protected virtual void ExtendHash(Hash128 hash) { }
protected virtual void ExtendHash(Hash128 hash)
{
}

protected abstract bool BuildSample();
protected abstract void pushSample(double timestamp = 0);

// Update is called once per frame
protected virtual void FixedUpdate()
{
if (moment == MomentForSampling.FixedUpdate && outlet != null)
{
if (BuildSample())
pushSample((timeSync != null) ? timeSync.FixedUpdateTimeStamp : 0.0);
}
if (moment != MomentForSampling.FixedUpdate || outlet == null) return;

if (!BuildSample()) return;

pushSample(TimeSync.Instance ? TimeSync.Instance.FixedUpdateTimestamp : 0.0);
}

protected virtual void Update()
{
if (outlet != null)
if (outlet == null) return;

if (!BuildSample()) return;

if (moment == MomentForSampling.Update)
{
if (BuildSample())
{
if (moment == MomentForSampling.Update)
{
pushSample((timeSync != null) ? timeSync.UpdateTimeStamp : 0.0);
}
else if (moment == MomentForSampling.EndOfFrame)
{
// Send as close to render-time as possible. Use this to log stimulus events.
StartCoroutine(PushAfterRendered());
}
}
pushSample(TimeSync.Instance ? TimeSync.Instance.UpdateTimestamp : 0.0);
}
else if (moment == MomentForSampling.EndOfFrame)
{
// Send as close to render-time as possible. Use this to log stimulus events.
StartCoroutine(PushAfterRendered());
}
}

protected virtual void LateUpdate()
{
if (moment == MomentForSampling.LateUpdate && outlet != null)
{
if (BuildSample())
pushSample((timeSync != null) ? timeSync.LateUpdateTimeStamp : 0.0);
}
if (moment != MomentForSampling.LateUpdate || outlet == null) return;

if (!BuildSample()) return;

pushSample(TimeSync.Instance ? TimeSync.Instance.LateUpdateTimestamp : 0.0);
}

IEnumerator PushAfterRendered()
Expand All @@ -114,7 +115,11 @@ IEnumerator PushAfterRendered()

public abstract class AFloatOutlet : ABaseOutlet<float>
{
public override channel_format_t Format { get { return channel_format_t.cf_float32; } }
public override channel_format_t Format
{
get { return channel_format_t.cf_float32; }
}

protected override void pushSample(double timestamp = 0)
{
if (outlet == null)
Expand All @@ -125,7 +130,11 @@ protected override void pushSample(double timestamp = 0)

public abstract class ADoubleOutlet : ABaseOutlet<double>
{
public override channel_format_t Format { get { return channel_format_t.cf_double64; } }
public override channel_format_t Format
{
get { return channel_format_t.cf_double64; }
}

protected override void pushSample(double timestamp = 0)
{
if (outlet == null)
Expand All @@ -136,7 +145,11 @@ protected override void pushSample(double timestamp = 0)

public abstract class AIntOutlet : ABaseOutlet<int>
{
public override channel_format_t Format { get { return channel_format_t.cf_int32; } }
public override channel_format_t Format
{
get { return channel_format_t.cf_int32; }
}

protected override void pushSample(double timestamp = 0)
{
if (outlet == null)
Expand All @@ -147,7 +160,11 @@ protected override void pushSample(double timestamp = 0)

public abstract class ACharOutlet : ABaseOutlet<char>
{
public override channel_format_t Format { get { return channel_format_t.cf_int8; } }
public override channel_format_t Format
{
get { return channel_format_t.cf_int8; }
}

protected override void pushSample(double timestamp = 0)
{
if (outlet == null)
Expand All @@ -158,7 +175,11 @@ protected override void pushSample(double timestamp = 0)

public abstract class AStringOutlet : ABaseOutlet<string>
{
public override channel_format_t Format { get { return channel_format_t.cf_string; } }
public override channel_format_t Format
{
get { return channel_format_t.cf_string; }
}

protected override void pushSample(double timestamp = 0)
{
if (outlet == null)
Expand All @@ -169,7 +190,11 @@ protected override void pushSample(double timestamp = 0)

public abstract class AShortOutlet : ABaseOutlet<short>
{
public override channel_format_t Format { get { return channel_format_t.cf_int16; } }
public override channel_format_t Format
{
get { return channel_format_t.cf_int16; }
}

protected override void pushSample(double timestamp = 0)
{
if (outlet == null)
Expand Down
88 changes: 46 additions & 42 deletions Runtime/Scripts/TimeSync.cs
Original file line number Diff line number Diff line change
@@ -1,67 +1,71 @@
using UnityEngine;


namespace LSL4Unity.Utils
{
/// <summary>
/// This singleton should provide an dedicated timestamp for each update call or fixed update LSL sample!
/// So that each sample provided by an Unity3D app has the same timestamp
/// Important! Make sure that the script is called before the default execution order!
/// A singleton to provide a dedicated timestamp for each update call: FixedUpdate, Update and LateUpdate.
/// Each sample provide by a Unity app should have the same timestamp.
/// </summary>
[ScriptOrder( -1000 )]
public class TimeSync : MonoBehaviour
/// <remarks>
/// Important! Make sure that the Update methods within the class are called before the default execution order!
/// The ScriptOrder attribute takes care of that for the user.
/// </remarks>
[ScriptOrder(-1000)]
public sealed class TimeSync : MonoBehaviour
{
private static TimeSync instance;
public static TimeSync Instance
{
get
{
return instance;
}
}
[Tooltip("Flag for making the script not destroyable on load.")] [SerializeField]
private bool dontDestroyOnLoad = false;

private double fixedUpdateTimeStamp;
public double FixedUpdateTimeStamp
{
get
{
return fixedUpdateTimeStamp;
}
}
/// <summary>
/// Singleton instance of TimeSync class
/// </summary>
public static TimeSync Instance { get; private set; }

/// <summary>
/// Timestamp at FixedUpdate
/// </summary>
public double FixedUpdateTimestamp { get; private set; }

private double updateTimeStamp;
public double UpdateTimeStamp
/// <summary>
/// Timestamp at Update
/// </summary>
public double UpdateTimestamp { get; private set; }

/// <summary>
/// Timestamp at LateUpdate
/// </summary>
public double LateUpdateTimestamp { get; private set; }


private void Awake()
{
get
// Create Singleton or destroy if an instance already exists
if (Instance && Instance != this)
{
return updateTimeStamp;
Destroy(this);
}
}

private double lateUpdateTimeStamp;
public double LateUpdateTimeStamp
{
get { return lateUpdateTimeStamp; }
}
else
{
Instance = this;

void Awake()
{
TimeSync.instance = this;
// Set don't destroy on load based on flag
if (dontDestroyOnLoad) DontDestroyOnLoad(this);
}
}

void FixedUpdate()
private void FixedUpdate()
{
fixedUpdateTimeStamp = LSL.LSL.local_clock();
FixedUpdateTimestamp = LSL.LSL.local_clock();
}

void Update()
private void Update()
{
updateTimeStamp = LSL.LSL.local_clock();
UpdateTimestamp = LSL.LSL.local_clock();
}

void LateUpdate()
private void LateUpdate()
{
lateUpdateTimeStamp = LSL.LSL.local_clock();
LateUpdateTimestamp = LSL.LSL.local_clock();
}
}
}
2 changes: 1 addition & 1 deletion Runtime/Scripts/TimeSync.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "com.labstreaminglayer.lsl4unity",
"version": "1.16.0",
"version": "1.16.1",
"description": "labstreaminglayer for Unity",
"displayName": "labstreaminglayer for Unity",
"unity": "2020.3",
Expand Down