mirror of https://github.com/m-labs/artiq.git
Move mu_to_seconds, seconds_to_mu to Core.
This commit is contained in:
parent
06ea76336d
commit
009d396740
|
@ -203,12 +203,6 @@ def fn_delay_mu():
|
||||||
def fn_at_mu():
|
def fn_at_mu():
|
||||||
return types.TBuiltinFunction("at_mu")
|
return types.TBuiltinFunction("at_mu")
|
||||||
|
|
||||||
def fn_mu_to_seconds():
|
|
||||||
return types.TBuiltinFunction("mu_to_seconds")
|
|
||||||
|
|
||||||
def fn_seconds_to_mu():
|
|
||||||
return types.TBuiltinFunction("seconds_to_mu")
|
|
||||||
|
|
||||||
def fn_rtio_log():
|
def fn_rtio_log():
|
||||||
return types.TBuiltinFunction("rtio_log")
|
return types.TBuiltinFunction("rtio_log")
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,6 @@ def globals():
|
||||||
"now_mu": builtins.fn_now_mu(),
|
"now_mu": builtins.fn_now_mu(),
|
||||||
"delay_mu": builtins.fn_delay_mu(),
|
"delay_mu": builtins.fn_delay_mu(),
|
||||||
"at_mu": builtins.fn_at_mu(),
|
"at_mu": builtins.fn_at_mu(),
|
||||||
"mu_to_seconds": builtins.fn_mu_to_seconds(),
|
|
||||||
"seconds_to_mu": builtins.fn_seconds_to_mu(),
|
|
||||||
|
|
||||||
# ARTIQ utility functions
|
# ARTIQ utility functions
|
||||||
"rtio_log": builtins.fn_rtio_log(),
|
"rtio_log": builtins.fn_rtio_log(),
|
||||||
|
|
|
@ -1731,20 +1731,6 @@ class ARTIQIRGenerator(algorithm.Visitor):
|
||||||
or types.is_builtin(typ, "at_mu"):
|
or types.is_builtin(typ, "at_mu"):
|
||||||
return self.append(ir.Builtin(typ.name,
|
return self.append(ir.Builtin(typ.name,
|
||||||
[self.visit(arg) for arg in node.args], node.type))
|
[self.visit(arg) for arg in node.args], node.type))
|
||||||
elif types.is_builtin(typ, "mu_to_seconds"):
|
|
||||||
if len(node.args) == 1 and len(node.keywords) == 0:
|
|
||||||
arg = self.visit(node.args[0])
|
|
||||||
arg_float = self.append(ir.Coerce(arg, builtins.TFloat()))
|
|
||||||
return self.append(ir.Arith(ast.Mult(loc=None), arg_float, self.ref_period))
|
|
||||||
else:
|
|
||||||
assert False
|
|
||||||
elif types.is_builtin(typ, "seconds_to_mu"):
|
|
||||||
if len(node.args) == 1 and len(node.keywords) == 0:
|
|
||||||
arg = self.visit(node.args[0])
|
|
||||||
arg_mu = self.append(ir.Arith(ast.Div(loc=None), arg, self.ref_period))
|
|
||||||
return self.append(ir.Coerce(arg_mu, builtins.TInt64()))
|
|
||||||
else:
|
|
||||||
assert False
|
|
||||||
elif types.is_exn_constructor(typ):
|
elif types.is_exn_constructor(typ):
|
||||||
return self.alloc_exn(node.type, *[self.visit(arg_node) for arg_node in node.args])
|
return self.alloc_exn(node.type, *[self.visit(arg_node) for arg_node in node.args])
|
||||||
elif types.is_constructor(typ):
|
elif types.is_constructor(typ):
|
||||||
|
|
|
@ -899,12 +899,6 @@ class Inferencer(algorithm.Visitor):
|
||||||
elif types.is_builtin(typ, "at_mu"):
|
elif types.is_builtin(typ, "at_mu"):
|
||||||
simple_form("at_mu(time_mu:numpy.int64) -> None",
|
simple_form("at_mu(time_mu:numpy.int64) -> None",
|
||||||
[builtins.TInt64()])
|
[builtins.TInt64()])
|
||||||
elif types.is_builtin(typ, "mu_to_seconds"):
|
|
||||||
simple_form("mu_to_seconds(time_mu:numpy.int64) -> float",
|
|
||||||
[builtins.TInt64()], builtins.TFloat())
|
|
||||||
elif types.is_builtin(typ, "seconds_to_mu"):
|
|
||||||
simple_form("seconds_to_mu(time:float) -> numpy.int64",
|
|
||||||
[builtins.TFloat()], builtins.TInt64())
|
|
||||||
elif types.is_builtin(typ, "watchdog"):
|
elif types.is_builtin(typ, "watchdog"):
|
||||||
simple_form("watchdog(time:float) -> [builtin context manager]",
|
simple_form("watchdog(time:float) -> [builtin context manager]",
|
||||||
[builtins.TFloat()], builtins.TNone())
|
[builtins.TFloat()], builtins.TNone())
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from artiq.language.core import (kernel, portable, delay_mu, delay,
|
from artiq.language.core import (kernel, portable, delay_mu, delay)
|
||||||
seconds_to_mu)
|
|
||||||
from artiq.language.units import ns, us
|
from artiq.language.units import ns, us
|
||||||
from artiq.coredevice import spi
|
from artiq.coredevice import spi
|
||||||
|
|
||||||
|
@ -166,10 +165,10 @@ class AD5360:
|
||||||
self.bus.write_period_mu +
|
self.bus.write_period_mu +
|
||||||
self.bus.ref_period_mu) -
|
self.bus.ref_period_mu) -
|
||||||
3*self.bus.ref_period_mu -
|
3*self.bus.ref_period_mu -
|
||||||
seconds_to_mu(1.5*us))
|
self.core.seconds_to_mu(1.5*us))
|
||||||
for i in range(len(values)):
|
for i in range(len(values)):
|
||||||
self.write_channel(i, values[i], op)
|
self.write_channel(i, values[i], op)
|
||||||
delay_mu(3*self.bus.ref_period_mu + # latency alignment ttl to spi
|
delay_mu(3*self.bus.ref_period_mu + # latency alignment ttl to spi
|
||||||
seconds_to_mu(1.5*us)) # t10 max busy low for one channel
|
self.core.seconds_to_mu(1.5*us)) # t10 max busy low for one channel
|
||||||
self.load()
|
self.load()
|
||||||
delay_mu(-2*self.bus.ref_period_mu) # load(), t13
|
delay_mu(-2*self.bus.ref_period_mu) # load(), t13
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import os, sys
|
import os, sys
|
||||||
|
import numpy
|
||||||
|
|
||||||
from pythonparser import diagnostic
|
from pythonparser import diagnostic
|
||||||
|
|
||||||
|
@ -124,6 +125,23 @@ class Core:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@portable
|
||||||
|
def seconds_to_mu(self, seconds):
|
||||||
|
"""Converts seconds to the corresponding number of machine units
|
||||||
|
(RTIO cycles).
|
||||||
|
|
||||||
|
:param seconds: time (in seconds) to convert.
|
||||||
|
"""
|
||||||
|
return numpy.int64(seconds//self.ref_period)
|
||||||
|
|
||||||
|
@portable
|
||||||
|
def mu_to_seconds(self, mu):
|
||||||
|
"""Converts machine units (RTIO cycles) to seconds.
|
||||||
|
|
||||||
|
:param mu: cycle count to convert.
|
||||||
|
"""
|
||||||
|
return mu*self.ref_period
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def get_rtio_counter_mu(self):
|
def get_rtio_counter_mu(self):
|
||||||
return rtio_get_counter()
|
return rtio_get_counter()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
from artiq.language.core import (kernel, portable, seconds_to_mu, now_mu,
|
from artiq.language.core import (kernel, portable, now_mu, delay_mu)
|
||||||
delay_mu, mu_to_seconds)
|
|
||||||
from artiq.language.units import MHz
|
from artiq.language.units import MHz
|
||||||
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
||||||
|
|
||||||
|
@ -59,8 +58,7 @@ class SPIMaster:
|
||||||
"""
|
"""
|
||||||
def __init__(self, dmgr, channel, core_device="core"):
|
def __init__(self, dmgr, channel, core_device="core"):
|
||||||
self.core = dmgr.get(core_device)
|
self.core = dmgr.get(core_device)
|
||||||
self.ref_period_mu = seconds_to_mu(self.core.coarse_ref_period,
|
self.ref_period_mu = self.core.seconds_to_mu(self.core.coarse_ref_period)
|
||||||
self.core)
|
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.write_period_mu = numpy.int64(0)
|
self.write_period_mu = numpy.int64(0)
|
||||||
self.read_period_mu = numpy.int64(0)
|
self.read_period_mu = numpy.int64(0)
|
||||||
|
@ -68,7 +66,7 @@ class SPIMaster:
|
||||||
|
|
||||||
@portable
|
@portable
|
||||||
def frequency_to_div(self, f):
|
def frequency_to_div(self, f):
|
||||||
return int(1/(f*mu_to_seconds(self.ref_period_mu))) + 1
|
return int(1/(f*self.core.mu_to_seconds(self.ref_period_mu))) + 1
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def set_config(self, flags=0, write_freq=20*MHz, read_freq=20*MHz):
|
def set_config(self, flags=0, write_freq=20*MHz, read_freq=20*MHz):
|
||||||
|
|
|
@ -94,7 +94,7 @@ class _Frame:
|
||||||
|
|
||||||
def _arm(self):
|
def _arm(self):
|
||||||
self.segment_delays = [
|
self.segment_delays = [
|
||||||
seconds_to_mu(s.duration*delay_margin_factor, self.core)
|
self.core.seconds_to_mu(s.duration*delay_margin_factor)
|
||||||
for s in self.segments]
|
for s in self.segments]
|
||||||
|
|
||||||
def _invalidate(self):
|
def _invalidate(self):
|
||||||
|
@ -125,7 +125,7 @@ class _Frame:
|
||||||
raise ArmError()
|
raise ArmError()
|
||||||
|
|
||||||
call_t = now_mu()
|
call_t = now_mu()
|
||||||
trigger_start_t = call_t - seconds_to_mu(trigger_duration/2)
|
trigger_start_t = call_t - self.core.seconds_to_mu(trigger_duration/2)
|
||||||
|
|
||||||
if self.pdq.current_frame >= 0:
|
if self.pdq.current_frame >= 0:
|
||||||
# PDQ is in the middle of a frame. Check it is us.
|
# PDQ is in the middle of a frame. Check it is us.
|
||||||
|
@ -136,7 +136,7 @@ class _Frame:
|
||||||
# to play our first segment.
|
# to play our first segment.
|
||||||
self.pdq.current_frame = self.frame_number
|
self.pdq.current_frame = self.frame_number
|
||||||
self.pdq.next_segment = 0
|
self.pdq.next_segment = 0
|
||||||
at_mu(trigger_start_t - seconds_to_mu(frame_setup))
|
at_mu(trigger_start_t - self.core.seconds_to_mu(frame_setup))
|
||||||
self.pdq.frame0.set_o(bool(self.frame_number & 1))
|
self.pdq.frame0.set_o(bool(self.frame_number & 1))
|
||||||
self.pdq.frame1.set_o(bool((self.frame_number & 2) >> 1))
|
self.pdq.frame1.set_o(bool((self.frame_number & 2) >> 1))
|
||||||
self.pdq.frame2.set_o(bool((self.frame_number & 4) >> 2))
|
self.pdq.frame2.set_o(bool((self.frame_number & 4) >> 2))
|
||||||
|
|
|
@ -8,7 +8,7 @@ class IdleKernel(EnvExperiment):
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def run(self):
|
def run(self):
|
||||||
start_time = now_mu() + seconds_to_mu(500*ms)
|
start_time = now_mu() + self.core.seconds_to_mu(500*ms)
|
||||||
while self.core.get_rtio_counter_mu() < start_time:
|
while self.core.get_rtio_counter_mu() < start_time:
|
||||||
pass
|
pass
|
||||||
self.core.reset()
|
self.core.reset()
|
||||||
|
|
|
@ -42,7 +42,7 @@ class TDR(EnvExperiment):
|
||||||
pulse = 1e-6 # pulse length, larger than rtt
|
pulse = 1e-6 # pulse length, larger than rtt
|
||||||
self.t = [0 for i in range(2)]
|
self.t = [0 for i in range(2)]
|
||||||
try:
|
try:
|
||||||
self.many(n, seconds_to_mu(pulse, self.core))
|
self.many(n, self.core.seconds_to_mu(pulse))
|
||||||
except PulseNotReceivedError:
|
except PulseNotReceivedError:
|
||||||
print("to few edges: cable too long or wiring bad")
|
print("to few edges: cable too long or wiring bad")
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -15,7 +15,6 @@ __all__ = ["kernel", "portable", "rpc", "syscall", "host_only",
|
||||||
kernel_globals = (
|
kernel_globals = (
|
||||||
"sequential", "parallel", "interleave",
|
"sequential", "parallel", "interleave",
|
||||||
"delay_mu", "now_mu", "at_mu", "delay",
|
"delay_mu", "now_mu", "at_mu", "delay",
|
||||||
"seconds_to_mu", "mu_to_seconds",
|
|
||||||
"watchdog"
|
"watchdog"
|
||||||
)
|
)
|
||||||
__all__.extend(kernel_globals)
|
__all__.extend(kernel_globals)
|
||||||
|
@ -213,31 +212,6 @@ def delay(duration):
|
||||||
_time_manager.take_time(duration)
|
_time_manager.take_time(duration)
|
||||||
|
|
||||||
|
|
||||||
def seconds_to_mu(seconds, core=None):
|
|
||||||
"""Converts seconds to the corresponding number of machine units
|
|
||||||
(RTIO cycles).
|
|
||||||
|
|
||||||
:param seconds: time (in seconds) to convert.
|
|
||||||
:param core: core device for which to perform the conversion. Specify only
|
|
||||||
when running in the interpreter (not in kernel).
|
|
||||||
"""
|
|
||||||
if core is None:
|
|
||||||
raise ValueError("Core device must be specified for time conversion")
|
|
||||||
return numpy.int64(seconds//core.ref_period)
|
|
||||||
|
|
||||||
|
|
||||||
def mu_to_seconds(mu, core=None):
|
|
||||||
"""Converts machine units (RTIO cycles) to seconds.
|
|
||||||
|
|
||||||
:param mu: cycle count to convert.
|
|
||||||
:param core: core device for which to perform the conversion. Specify only
|
|
||||||
when running in the interpreter (not in kernel).
|
|
||||||
"""
|
|
||||||
if core is None:
|
|
||||||
raise ValueError("Core device must be specified for time conversion")
|
|
||||||
return mu*core.ref_period
|
|
||||||
|
|
||||||
|
|
||||||
class _DummyWatchdog:
|
class _DummyWatchdog:
|
||||||
def __init__(self, timeout):
|
def __init__(self, timeout):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from random import Random
|
from random import Random
|
||||||
|
import numpy
|
||||||
|
|
||||||
from artiq.language.core import delay, at_mu, kernel
|
from artiq.language.core import delay, at_mu, kernel
|
||||||
from artiq.sim import time
|
from artiq.sim import time
|
||||||
|
@ -18,6 +19,12 @@ class Core:
|
||||||
time.manager.timeline.clear()
|
time.manager.timeline.clear()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
def seconds_to_mu(self, seconds):
|
||||||
|
return numpy.int64(seconds//self.ref_period)
|
||||||
|
|
||||||
|
def mu_to_seconds(self, mu):
|
||||||
|
return mu*self.ref_period
|
||||||
|
|
||||||
|
|
||||||
class Input:
|
class Input:
|
||||||
def __init__(self, dmgr, name):
|
def __init__(self, dmgr, name):
|
||||||
|
|
|
@ -83,7 +83,7 @@ class _PulseLogger(EnvExperiment):
|
||||||
if not hasattr(self.parent_test, "first_timestamp"):
|
if not hasattr(self.parent_test, "first_timestamp"):
|
||||||
self.parent_test.first_timestamp = t
|
self.parent_test.first_timestamp = t
|
||||||
origin = self.parent_test.first_timestamp
|
origin = self.parent_test.first_timestamp
|
||||||
t_usec = round(mu_to_seconds(t-origin, self.core)*1000000)
|
t_usec = round(self.core.mu_to_seconds(t-origin)*1000000)
|
||||||
self.parent_test.output_list.append((self.name, t_usec, l, f))
|
self.parent_test.output_list.append((self.name, t_usec, l, f))
|
||||||
|
|
||||||
def on(self, t, f):
|
def on(self, t, f):
|
||||||
|
|
|
@ -38,7 +38,7 @@ class RTT(EnvExperiment):
|
||||||
t1 = self.ttl_inout.timestamp_mu()
|
t1 = self.ttl_inout.timestamp_mu()
|
||||||
if t1 < 0:
|
if t1 < 0:
|
||||||
raise PulseNotReceived()
|
raise PulseNotReceived()
|
||||||
self.set_dataset("rtt", mu_to_seconds(t1 - t0))
|
self.set_dataset("rtt", self.core.mu_to_seconds(t1 - t0))
|
||||||
|
|
||||||
|
|
||||||
class Loopback(EnvExperiment):
|
class Loopback(EnvExperiment):
|
||||||
|
@ -62,7 +62,7 @@ class Loopback(EnvExperiment):
|
||||||
t1 = self.loop_in.timestamp_mu()
|
t1 = self.loop_in.timestamp_mu()
|
||||||
if t1 < 0:
|
if t1 < 0:
|
||||||
raise PulseNotReceived()
|
raise PulseNotReceived()
|
||||||
self.set_dataset("rtt", mu_to_seconds(t1 - t0))
|
self.set_dataset("rtt", self.core.mu_to_seconds(t1 - t0))
|
||||||
|
|
||||||
|
|
||||||
class ClockGeneratorLoopback(EnvExperiment):
|
class ClockGeneratorLoopback(EnvExperiment):
|
||||||
|
@ -93,7 +93,7 @@ class PulseRate(EnvExperiment):
|
||||||
@kernel
|
@kernel
|
||||||
def run(self):
|
def run(self):
|
||||||
self.core.reset()
|
self.core.reset()
|
||||||
dt = seconds_to_mu(300*ns)
|
dt = self.core.seconds_to_mu(300*ns)
|
||||||
while True:
|
while True:
|
||||||
for i in range(10000):
|
for i in range(10000):
|
||||||
try:
|
try:
|
||||||
|
@ -104,7 +104,7 @@ class PulseRate(EnvExperiment):
|
||||||
self.core.break_realtime()
|
self.core.break_realtime()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.set_dataset("pulse_rate", mu_to_seconds(dt))
|
self.set_dataset("pulse_rate", self.core.mu_to_seconds(dt))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ class PulseRateDDS(EnvExperiment):
|
||||||
@kernel
|
@kernel
|
||||||
def run(self):
|
def run(self):
|
||||||
self.core.reset()
|
self.core.reset()
|
||||||
dt = seconds_to_mu(5*us)
|
dt = self.core.seconds_to_mu(5*us)
|
||||||
while True:
|
while True:
|
||||||
delay(10*ms)
|
delay(10*ms)
|
||||||
for i in range(1250):
|
for i in range(1250):
|
||||||
|
@ -132,7 +132,7 @@ class PulseRateDDS(EnvExperiment):
|
||||||
self.core.break_realtime()
|
self.core.break_realtime()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.set_dataset("pulse_rate", mu_to_seconds(dt//2))
|
self.set_dataset("pulse_rate", self.core.mu_to_seconds(dt//2))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@ -403,7 +403,7 @@ class CoredeviceTest(ExperimentCase):
|
||||||
self.execute(TimeKeepsRunning)
|
self.execute(TimeKeepsRunning)
|
||||||
t2 = self.dataset_mgr.get("time_at_start")
|
t2 = self.dataset_mgr.get("time_at_start")
|
||||||
|
|
||||||
dead_time = mu_to_seconds(t2 - t1, self.device_mgr.get("core"))
|
dead_time = self.core.mu_to_seconds(t2 - t1, self.device_mgr.get("core"))
|
||||||
print(dead_time)
|
print(dead_time)
|
||||||
self.assertGreater(dead_time, 1*ms)
|
self.assertGreater(dead_time, 1*ms)
|
||||||
self.assertLess(dead_time, 2500*ms)
|
self.assertLess(dead_time, 2500*ms)
|
||||||
|
@ -434,7 +434,7 @@ class RPCTiming(EnvExperiment):
|
||||||
t1 = self.core.get_rtio_counter_mu()
|
t1 = self.core.get_rtio_counter_mu()
|
||||||
self.nop()
|
self.nop()
|
||||||
t2 = self.core.get_rtio_counter_mu()
|
t2 = self.core.get_rtio_counter_mu()
|
||||||
self.ts[i] = mu_to_seconds(t2 - t1)
|
self.ts[i] = self.core.mu_to_seconds(t2 - t1)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.ts = [0. for _ in range(self.repeats)]
|
self.ts = [0. for _ in range(self.repeats)]
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# RUN: %python -m artiq.compiler.testbench.jit %s
|
|
||||||
# REQUIRES: time
|
|
||||||
|
|
||||||
assert seconds_to_mu(2.0) == 2000000
|
|
||||||
assert mu_to_seconds(1500000) == 1.5
|
|
|
@ -151,7 +151,7 @@ In the synthetic example above, the compiler will be able to detect that the res
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def loop(self):
|
def loop(self):
|
||||||
precomputed_delay_mu = seconds_to_mu(self.worker.interval / 5.0)
|
precomputed_delay_mu = self.core.seconds_to_mu(self.worker.interval / 5.0)
|
||||||
for _ in range(100):
|
for _ in range(100):
|
||||||
delay_mu(precomputed_delay_mu)
|
delay_mu(precomputed_delay_mu)
|
||||||
self.worker.work()
|
self.worker.work()
|
||||||
|
|
|
@ -36,7 +36,7 @@ The wall clock keeps running across experiments.
|
||||||
Absolute timestamps can be large numbers.
|
Absolute timestamps can be large numbers.
|
||||||
They are represented internally as 64-bit integers with a resolution of typically a nanosecond and a range of hundreds of years.
|
They are represented internally as 64-bit integers with a resolution of typically a nanosecond and a range of hundreds of years.
|
||||||
Conversions between such a large integer number and a floating point representation can cause loss of precision through cancellation.
|
Conversions between such a large integer number and a floating point representation can cause loss of precision through cancellation.
|
||||||
When computing the difference of absolute timestamps, use ``mu_to_seconds(t2-t1)``, not ``mu_to_seconds(t2)-mu_to_seconds(t1)`` (see :meth:`artiq.language.core.mu_to_seconds`).
|
When computing the difference of absolute timestamps, use ``self.core.mu_to_seconds(t2-t1)``, not ``self.core.mu_to_seconds(t2)-self.core.mu_to_seconds(t1)`` (see :meth:`artiq.coredevice.Core.mu_to_seconds`).
|
||||||
When accumulating time, do it in machine units and not in SI units, so that rounding errors do not accumulate.
|
When accumulating time, do it in machine units and not in SI units, so that rounding errors do not accumulate.
|
||||||
|
|
||||||
The following basic example shows how to place output events on the timeline.
|
The following basic example shows how to place output events on the timeline.
|
||||||
|
|
Loading…
Reference in New Issue