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
|