corrscope/ovgenpy/renderer.py

152 wiersze
4.2 KiB
Python
Czysty Zwykły widok Historia

from itertools import count
from typing import NamedTuple, Optional, List, Tuple
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from matplotlib.lines import Line2D
from ovgenpy.util import ceildiv
from ovgenpy.wave import Wave
class RendererConfig(NamedTuple):
width: int
height: int
samples_visible: int
rows_first: bool
nrows: Optional[int] = None
ncols: Optional[int] = None
# TODO backend: FigureCanvasBase = FigureCanvasAgg
class MatplotlibRenderer:
"""
TODO disable antialiasing
If __init__ reads cfg, cfg cannot be hotswapped.
Reasons to hotswap cfg: RendererCfg:
- GUI preview size
- Changing layout
- Changing #smp drawn (samples_visible)
(see RendererCfg)
Original OVGen does not support hotswapping.
It disables changing options during rendering.
Reasons to hotswap trigger algorithms:
- changing scan_nsamp (cannot be hotswapped, since correlation buffer is incompatible)
So don't.
"""
DPI = 96
def __init__(self, cfg: RendererConfig, waves: List[Wave]):
self.cfg = cfg
self.waves = waves
self.fig: Figure = None
# Setup layout
self.nrows = 0
self.ncols = 0
# Flat array of nrows*ncols elements, ordered by cfg.rows_first.
self.axes: List[Axes] = None
self.lines: List[Line2D] = None
self.set_layout() # mutates self
def set_layout(self) -> None:
"""
Inputs: self.cfg, self.waves, self.fig
Outputs: self.nrows, self.ncols, self.axes
Creates a flat array of Matplotlib Axes, with the new layout.
"""
self.nrows, self.ncols = self.calc_layout()
# Create Axes
# https://matplotlib.org/api/_as_gen/matplotlib.pyplot.subplots.html
if self.fig:
plt.close(self.fig) # FIXME
axes2d: np.ndarray[Axes]
self.fig, axes2d = plt.subplots(
self.nrows, self.ncols,
squeeze=False,
# Remove gaps between Axes
gridspec_kw=dict(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
)
# remove Axis from Axes
for ax in axes2d.flatten():
ax.set_axis_off()
# if column major:
if not self.cfg.rows_first:
axes2d = axes2d.T
nwave = len(self.waves)
self.axes: List[Axes] = axes2d.flatten().tolist()[:nwave]
# Create oscilloscope line objects
self.lines = []
for ax in self.axes:
# Setup axes limits
ax.set_xlim(0, self.cfg.samples_visible)
ax.set_ylim(-1, 1)
line = ax.plot([0] * self.cfg.samples_visible)[0]
self.lines.append(line)
# Setup figure geometry
self.fig.set_dpi(self.DPI)
self.fig.set_size_inches(
self.cfg.width / self.DPI,
self.cfg.height / self.DPI
)
plt.show(block=False)
def calc_layout(self) -> Tuple[int, int]:
"""
Inputs: self.cfg, self.waves
:return: (nrows, ncols)
"""
cfg = self.cfg
nwaves = len(self.waves)
if cfg.rows_first:
nrows = cfg.nrows
if nrows is None:
raise ValueError('invalid cfg: rows_first is True and nrows is None')
ncols = ceildiv(nwaves, nrows)
else:
ncols = cfg.ncols
if ncols is None:
raise ValueError('invalid cfg: rows_first is False and ncols is None')
nrows = ceildiv(nwaves, ncols)
return nrows, ncols
def render_frame(self, center_smps: List[int]) -> None:
nwaves = len(self.waves)
ncenters = len(center_smps)
if nwaves != ncenters:
raise ValueError(
f'incorrect wave offsets: {nwaves} waves but {ncenters} offsets')
for idx, wave, center_smp in zip(count(), self.waves, center_smps):
# Draw waveform data
line = self.lines[idx]
data = wave.get_around(center_smp, self.cfg.samples_visible)
line.set_ydata(data)
self.fig.canvas.draw()
self.fig.canvas.flush_events()