diff --git a/corrscope/config.py b/corrscope/config.py index 29212e9..b1788f8 100644 --- a/corrscope/config.py +++ b/corrscope/config.py @@ -1,3 +1,8 @@ +""" +The most important class in this module is `DumpableAttrs`. +See its docstring for details. +""" + import pickle import warnings from enum import Enum @@ -113,8 +118,73 @@ def copy_config(obj: T) -> T: class DumpableAttrs: """ Marks class as attrs, and enables YAML dumping (excludes default fields). - - Subclass parameter `always_dump` contains - whitespace-separated list of fields to always dump. + + It is subclassed for + (statically-typed key-value objects which will be dumped/loaded as YAML files). + + ## Subclassing `DumpableAttrs` and converting to YAML + + - This class works like `dataclasses` or `attrs.dataclass` decorators, + and wraps the latter. + - Subclass `DumpableAttrs`, then add class-level type annotations. + These annotations are converted into `__init__(...)` constructor parameters. + + ```py + class Config(DumpableAttrs): + path: str + priority: int = 0 + ``` + + Unlike many other libraries and usual JSON, + the YAML string representation of a `DumpableAttrs` object + encodes what Python type the object is. + For example, Config("foo.wav", 1) is dumped as: + + ```yaml + !Config + path: foo.wav # Required + priority: 1 # If not present, defaults to 0 + # See DumpableAttrs docstring for details. + ``` + + ## Polymorphism + + The YAML file determines what type is loaded, + so the config type can be used to pick a object type at runtime. + + - For example, putting `!CorrelationTriggerConfig` in YAML + loads a `CorrelationTriggerConfig` config object, + which tells corrscope to create a `CorrelationTrigger` algorithm object. + - Putting `!NullTriggerConfig` in YAML + instead loads a `NullTriggerConfig` config object, + which tells corrscope to create a `NullTrigger` algorithm object. + (This is only used for unit tests, and is incompatible with GUI.) + + ## `KeywordAttrs` are similar, with 2 differences: + + - Subclasses can have non-default arguments after default arguments. + - Its constructor can only be called with keyword (a=1, b=2) arguments, + not positional (1, 2). + + ## Optional Parameters + + class Config(DumpableAttrs, always_dump="", exclude=""): ... + - `always_dump` contains whitespace-separated list of fields to always dump + (if equal to default). + - If always_dump="*", `exclude` contains whitespace-separated list of fields + to not dump (if equal to default). + + ## Loading from YAML: Alias and Ignored + + YAML loading uses __getstate__ and __setstate__. + + If `old = Alias("new")`, + then loading a YAML file `old: value` initializes `new = value`. + + If `old = Ignored`, + then loading a YAML file `old: value` silently discards `value`. + Ignored is unused in my code, + since __setstate__ automatically discards unrecognized fields (with a warning). """ if TYPE_CHECKING: diff --git a/corrscope/designNotes.md b/corrscope/designNotes.md index 8ea4ca9..9464863 100644 --- a/corrscope/designNotes.md +++ b/corrscope/designNotes.md @@ -1,3 +1,47 @@ +# Module/Class Structure and Organization + +When corrscope is launched, it first executes `__main__.py` and `cli.py`. + +## cli.py + +If -w is present, it writes a .yaml file. If -p are present, it runs `corrscope.CorrScope` directly. If neither is present, it imports and runs the `gui` subdirectory. + +## CorrScope, Config + +`corrscope.py` defines classes `CorrScope`, `Config` (and `Arguments`). + +- `CorrScope` is the main loop of the program, and only communicates with the GUI through `Arguments`. `CorrScope` requires a `Config` and `Arguments`. + - `Arguments` is constructed by the GUI and used to update rendering progress dialogs, etc. + - `Config` is a dataclass (see `config.py`) which can be edited through the GUI, or loaded/saved to a YAML file. +- When `cli.py` creates new configs, `default_config()` is used as a template to supply values. When loading existing YAML files, only dataclass default values are used. + +----- + +`Config` holds `channels: List[ChannelConfig]`, which store all per-channel settings. + +`CorrScope` turns `channels` into a list of `Channel` objects. Each channel uses its own `ChannelConfig` parameters to create: + +- self.trigger_wave: Wave +- self.render_wave: Wave +- self.trigger: MainTrigger + +----- + +Each frame: + +`CorrScope` reads data from Wave objects, asks MainTrigger + +Each channel has its own `trigger_wave`, `render_wave`, and `trigger` instance. For each channel, `trigger_wave` is used by `trigger` to find a "trigger time" near the frame's center time. + +The `Renderer` and `Output` instances are shared among all channels. + +- `Renderer` turns a list of N-channel "wave file portions" (from `render_wave`) into a RGB video frame. +- `Output` sends a RGB video frame to ffmpeg, ffplay, or (in the future) an interactive GUI player window. + +## config.py + +See docstring at top of file. + # Design Notes (cross-cutting concerns) ## Renderer to Output