corrscope/ovgenpy/ovgenpy.py

220 wiersze
5.6 KiB
Python
Czysty Zwykły widok Historia

2018-07-12 22:27:26 +00:00
import weakref
from pathlib import Path
2018-07-13 02:29:05 +00:00
from typing import NamedTuple, Optional, List, Tuple
2018-07-12 22:27:26 +00:00
import click
2018-07-13 02:29:05 +00:00
import numpy as np
2018-07-12 22:27:26 +00:00
from scipy.io import wavfile
2018-07-13 00:48:51 +00:00
from ovgenpy.util import ceildiv
2018-07-12 22:27:26 +00:00
2018-07-13 00:48:51 +00:00
2018-07-12 22:27:26 +00:00
class Config(NamedTuple):
wave_dir: str # TODO remove, a function will expand wildcards and create List[WaveConfig]
master_wave: Optional[str]
fps: int
# TODO algorithm and twiddle knobs
2018-07-13 02:29:05 +00:00
render: 'RendererCfg'
class RendererCfg(NamedTuple):
width: int
height: int
samples_visible: int
rows_first: bool
nrows: Optional[int] = None
ncols: Optional[int] = None
2018-07-12 22:27:26 +00:00
Folder = click.Path(exists=True, file_okay=False)
File = click.Path(exists=True, dir_okay=False)
FPS = 60 # fps
@click.command()
@click.argument('wave_dir', type=Folder)
2018-07-13 02:29:05 +00:00
@click.option('--master-wave', type=File, default=None)
@click.option('--fps', default=FPS)
2018-07-12 22:27:26 +00:00
def main(wave_dir: str, master_wave: Optional[str], fps: int):
cfg = Config(
wave_dir=wave_dir,
master_wave=master_wave,
fps=fps,
2018-07-13 02:29:05 +00:00
render=RendererCfg( # todo
640, 360,
samples_visible=1000,
rows_first=False,
ncols=1
)
2018-07-12 22:27:26 +00:00
)
ovgen = Ovgen(cfg)
ovgen.write()
COLOR_CHANNELS = 3
class Ovgen:
def __init__(self, cfg: Config):
self.cfg = cfg
self.waves: List[Wave] = []
def write(self):
self.load_waves() # self.waves =
self.render()
def load_waves(self):
wave_dir = Path(self.cfg.wave_dir)
for idx, path in enumerate(wave_dir.glob('*.wav')):
wcfg = WaveConfig(
2018-07-13 02:29:05 +00:00
wave_path=str(path)
2018-07-12 22:27:26 +00:00
)
wave = Wave(wcfg, str(path))
self.waves.append(wave)
def render(self):
# Calculate number of frames (TODO master file?)
fps = self.cfg.fps
nframes = fps * self.waves[0].get_s()
nframes = int(nframes) + 1
2018-07-13 00:48:51 +00:00
renderer = MatplotlibRenderer(self.cfg.render, self.waves)
2018-07-12 22:27:26 +00:00
# For each frame, render each wave
for frame in range(nframes):
2018-07-13 00:48:51 +00:00
time_seconds = frame / fps
2018-07-12 22:27:26 +00:00
2018-07-13 00:48:51 +00:00
center_smps = []
2018-07-12 22:27:26 +00:00
for wave in self.waves:
2018-07-13 00:48:51 +00:00
sample = round(wave.smp_s * time_seconds)
2018-07-12 22:27:26 +00:00
trigger_sample = wave.trigger.get_trigger(sample)
2018-07-13 00:48:51 +00:00
center_smps.append(trigger_sample)
2018-07-12 22:27:26 +00:00
2018-07-13 00:48:51 +00:00
renderer.render_frame(center_smps)
2018-07-12 22:27:26 +00:00
class WaveConfig(NamedTuple):
wave_path: str
# TODO color
class Wave:
def __init__(self, wcfg: WaveConfig, wave_path: str):
2018-07-13 02:29:05 +00:00
self.cfg = wcfg
2018-07-12 22:27:26 +00:00
self.smp_s, self.data = wavfile.read(wave_path)
# FIXME cfg
frames = 1
self.trigger = Trigger(self, self.smp_s // FPS * frames, 0.1)
def get_smp(self) -> int:
return len(self.data)
def get_s(self) -> float:
"""
:return: time (seconds)
"""
return self.get_smp() / self.smp_s
class Trigger:
def __init__(self, wave: Wave, scan_nsamp: int, align_amount: float):
"""
2018-07-13 00:48:51 +00:00
Correlation-based trigger which looks at a window of `scan_nsamp` samples.
2018-07-12 22:27:26 +00:00
it's complicated
:param wave: Wave file
:param scan_nsamp: Number of samples used to align adjacent frames
:param align_amount: Amount of centering to apply to each frame, within [0, 1]
"""
# probably unnecessary
self.wave = weakref.proxy(wave)
self.scan_nsamp = scan_nsamp
self.align_amount = align_amount
def get_trigger(self, offset: int) -> int:
"""
:param offset: sample index
:return: new sample index, corresponding to rising edge
"""
return offset # todo
2018-07-13 00:48:51 +00:00
class MatplotlibRenderer:
2018-07-13 01:37:18 +00:00
def __init__(self, cfg: RendererCfg, waves: List[Wave]):
self.cfg = cfg
2018-07-13 00:48:51 +00:00
self.waves = waves
2018-07-13 02:29:05 +00:00
self.dims: Tuple[int, int] = (0, 0)
2018-07-13 01:37:18 +00:00
self.calc_layout()
2018-07-13 00:48:51 +00:00
"""
2018-07-13 01:37:18 +00:00
If __init__ reads cfg, cfg cannot be hotswapped.
2018-07-13 00:48:51 +00:00
2018-07-13 01:37:18 +00:00
Reasons to hotswap cfg: RendererCfg:
2018-07-13 00:48:51 +00:00
- GUI preview size
- Changing layout
- Changing #smp drawn (samples_visible)
(see RendererCfg)
2018-07-13 01:37:18 +00:00
Original OVGen does not support hotswapping.
It disables changing options during rendering.
2018-07-13 00:48:51 +00:00
Reasons to hotswap trigger algorithms:
- changing scan_nsamp (cannot be hotswapped, since correlation buffer is incompatible)
So don't.
"""
2018-07-13 01:37:18 +00:00
def calc_layout(self) -> None:
"""
Inputs: self.cfg, self.waves
2018-07-13 02:29:05 +00:00
Outputs: self.dims
2018-07-13 01:37:18 +00:00
"""
cfg = self.cfg
if cfg.rows_first:
2018-07-13 02:29:05 +00:00
major = cfg.nrows
if major is None:
2018-07-13 01:37:18 +00:00
raise ValueError('invalid cfg: rows_first is True and nrows is None')
2018-07-13 00:48:51 +00:00
else:
2018-07-13 02:29:05 +00:00
major = cfg.ncols
if major is None:
2018-07-13 01:37:18 +00:00
raise ValueError('invalid cfg: rows_first is False and ncols is None')
2018-07-13 00:48:51 +00:00
2018-07-13 02:29:05 +00:00
minor = ceildiv(len(self.waves), major)
self.dims = (major, minor)
2018-07-13 00:48:51 +00:00
def _get_coords(self, idx: int):
2018-07-13 02:29:05 +00:00
major, minor = np.unravel_index(idx, self.dims)
2018-07-13 01:37:18 +00:00
if self.cfg.rows_first:
2018-07-13 02:29:05 +00:00
row, col = major, minor
2018-07-13 01:37:18 +00:00
else:
2018-07-13 02:29:05 +00:00
col, row = major, minor
return Coords(row, col)
2018-07-13 00:48:51 +00:00
def render_frame(self, center_smps: List[int]) -> None:
nwaves = len(self.waves)
ncenters = len(center_smps)
if nwaves != ncenters:
raise ValueError(f'incorrect number of wave offsets: {nwaves} waves but {ncenters} offsets')
for wave, center_smp in zip(self.waves, center_smps): # TODO
print(wave)
print(center_smp)
print()
2018-07-13 02:29:05 +00:00
class Coords(NamedTuple):
row: int
col: int