Source code for ezmsg.xdf.source
import asyncio
import time
import typing
import ezmsg.core as ez
from ezmsg.baseproc.units import BaseProducerUnit
from ezmsg.util.messages.axisarray import AxisArray
from .iter import (
XDFAxisArrayIterator,
XDFIteratorSettings,
XDFMultiAxArrIterator,
XDFMultiIteratorSettings,
)
# The settings types moved to `iter.py` so the producers can own them, but they
# were importable from here first.
XDFMultiIteratorUnitSettings = XDFMultiIteratorSettings
__all__ = [
"PlaybackClock",
"XDFIteratorSettings",
"XDFIteratorUnit",
"XDFMultiIteratorSettings",
"XDFMultiIteratorUnit",
"XDFMultiIteratorUnitSettings",
]
[docs]
class PlaybackClock:
[docs]
def __init__(
self,
rate: float = 1.0,
step_dur: float = 0.005,
):
"""
Create an object that provides a timer that can run at a specified rate,
and with a specified step duration.
Args:
rate: Speed of playback. 1.0 is real time.
step_dur: The duration of each step in seconds.
Provide the duration using the unmodified rate.
"""
self._step_dur = step_dur * rate
self._wall_start: float = time.time() - self._step_dur / 2
self._step_count: int = 0
[docs]
def reset(self) -> None:
self._wall_start = time.time() - self._step_dur / 2
self._step_count = 0
def _get_duration(self) -> float:
wall_elapsed = time.time() - self._wall_start
next_elapsed = self._step_count * self._step_dur
step_dur = max(next_elapsed - wall_elapsed, 0)
self._step_count += 1
return step_dur
[docs]
async def astep(self) -> None:
await asyncio.sleep(self._get_duration())
[docs]
def step(self) -> None:
time.sleep(self._get_duration())
class _XDFUnitBase:
"""Playback pacing and end-of-file handling, shared by both units.
Note both subclasses name their publisher ``produce``: that is the name
``BaseProducerUnit`` uses, and ezmsg collects publishers per attribute, so a
differently named one would run *alongside* the base class\'s rather than
replacing it -- two publishers draining one producer, neither stopping.
The producer supplies chunks as fast as they can be sliced out of memory;
``playback_rate`` is what turns that into a paced stream, and it is a
property of the unit rather than of the reader.
"""
OUTPUT_TERM = ez.OutputStream(typing.Any)
async def initialize(self) -> None:
await super().initialize()
self._clock = (
PlaybackClock(rate=self.SETTINGS.playback_rate, step_dur=self.SETTINGS.chunk_dur)
if self.SETTINGS.playback_rate is not None
else None
)
async def _finish(self) -> typing.AsyncGenerator:
ez.logger.debug(f"File ({self.SETTINGS.filepath} :: {self.SETTINGS.select}) exhausted.")
if self.SETTINGS.self_terminating:
raise ez.NormalTermination
yield self.OUTPUT_TERM, True
[docs]
class XDFIteratorUnit(
_XDFUnitBase,
BaseProducerUnit[XDFIteratorSettings, AxisArray, XDFAxisArrayIterator],
):
SETTINGS = XDFIteratorSettings
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
[docs]
@ez.publisher(OUTPUT_SIGNAL)
async def produce(self) -> typing.AsyncGenerator:
while not self.producer.exhausted:
if self._clock is not None:
await self._clock.astep()
msg = await self.producer.__acall__()
if msg is not None and msg.data.size > 0:
yield self.OUTPUT_SIGNAL, msg
else:
await asyncio.sleep(0)
async for out in self._finish():
yield out
[docs]
class XDFMultiIteratorUnit(
_XDFUnitBase,
BaseProducerUnit[XDFMultiIteratorSettings, AxisArray, XDFMultiAxArrIterator],
):
SETTINGS = XDFMultiIteratorSettings
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
[docs]
@ez.publisher(OUTPUT_SIGNAL)
async def produce(self) -> typing.AsyncGenerator:
while not self.producer.exhausted:
if self._clock is not None:
await self._clock.astep()
msg = await self.producer.__acall__()
if msg is not None:
yield self.OUTPUT_SIGNAL, msg
else:
await asyncio.sleep(0)
async for out in self._finish():
yield out