def color2hex(color: Any) -> str: try: return matplotlib.colors.to_hex(color, keep_alpha=False) except ValueError: raise CorrError(f"invalid color {color}") except Exception as e: raise CorrError(f"doubly invalid color {color}, raises {e} (report bug!)")
def __attrs_post_init__(self): if self.edge_direction not in [-1, 1]: raise CorrError(f"{obj_name(self)}.edge_direction must be {{-1, 1}}") if self.post_trigger: self.post_trigger.parent = self if self.post_radius is None: name = obj_name(self) raise CorrError( f"Cannot supply {name}.post_trigger without supplying {name}.post_radius" )
def __init__(self, *args, **kwargs): Trigger.__init__(self, *args, **kwargs) if self._stride != 1: raise CorrError( f"{obj_name(self)} with stride != 1 is not allowed " f"(supplied {self._stride})") if self.post: raise CorrError( f"Passing {obj_name(self)} a post_trigger is not allowed " f"({obj_name(self.post)})")
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self._stride != 1: raise CorrError( f"{obj_name(self)} with stride != 1 is not allowed " f"(supplied {self._stride})")
def __init__(self, cfg: Config, arg: Arguments): """cfg is mutated! Recording config is triggered if any FFmpegOutputConfig is found. Preview mode is triggered if all outputs are FFplay or others. """ self.cfg = cfg self.arg = arg self.has_played = False # TODO test progress and is_aborted # TODO benchmark_mode/not_benchmarking == code duplication. benchmark_mode = self.cfg.benchmark_mode not_benchmarking = not benchmark_mode if not_benchmarking or benchmark_mode == BenchmarkMode.OUTPUT: self.output_cfgs = arg.outputs else: self.output_cfgs = [] # type: List[IOutputConfig] if len(self.cfg.channels) == 0: raise CorrError("Config.channels is empty") # Check for ffmpeg video recording, then mutate cfg. is_record = False for output in self.output_cfgs: if isinstance(output, outputs_.FFmpegOutputConfig): is_record = True break if is_record: self.cfg.before_record() else: self.cfg.before_preview()
def calc_flatten_matrix(flatten: FlattenOrStr, stereo_nchan: int) -> np.ndarray: """Raises CorrError on invalid input. If flatten is Flatten.Stereo, returns shape=(nchan,nchan) identity matrix. - (N,nchan) @ (nchan,nchan) = (N,nchan). Otherwise, returns shape=(nchan) flattening matrix. - (N,nchan) @ (nchan) = (N) https://docs.scipy.org/doc/numpy/reference/generated/numpy.matmul.html#numpy.matmul ''' If the second argument is 1-D, it is promoted to a matrix by appending a 1 to its dimensions. After matrix multiplication the appended 1 is removed." ''' """ if flatten is Flatten.Stereo: # 2D identity (results in 2-dim data) flatten_matrix = np.eye(stereo_nchan, dtype=FLOAT) # 1D (results in 1-dim data) elif flatten is Flatten.SumAvg: flatten_matrix = np.ones(stereo_nchan, dtype=FLOAT) / stereo_nchan elif flatten is Flatten.DiffAvg: flatten_matrix = calc_flatten_matrix(str(flatten), stereo_nchan) flatten_matrix = rightpad(flatten_matrix, stereo_nchan, 0) else: words = flatten.replace(",", " ").split() try: flatten_matrix = np.array([FLOAT(word) for word in words]) except ValueError as e: raise CorrError("Invalid stereo flattening matrix") from e flatten_abs_sum = np.sum(np.abs(flatten_matrix)) if flatten_abs_sum == 0: raise CorrError( "Stereo flattening matrix must have nonzero elements") flatten_matrix /= flatten_abs_sum assert flatten_matrix.dtype == FLOAT, flatten_matrix.dtype return flatten_matrix
def set(self: "ConfigModel", val: int): if val > 0: setattr(self.cfg.layout, altered, val) setattr(self.cfg.layout, unaltered, None) self.update_all_bound("layout__" + unaltered) elif val == 0: setattr(self.cfg.layout, altered, None) else: raise CorrError(f"invalid input: {altered} < 0, should never happen")
def __init__(self, cfg: ChannelConfig, corr_cfg: "Config"): self.cfg = cfg # Create a Wave object. wave = Wave( abspath(cfg.wav_path), amplification=coalesce(cfg.amplification, corr_cfg.amplification), ) # Flatten wave stereo for trigger and render. tflat = coalesce(cfg.trigger_stereo, corr_cfg.trigger_stereo) rflat = coalesce(cfg.render_stereo, corr_cfg.render_stereo) self.trigger_wave = wave.with_flatten(tflat, return_channels=False) self.render_wave = wave.with_flatten(rflat, return_channels=True) # `subsampling` increases `stride` and decreases `nsamp`. # `width` increases `stride` without changing `nsamp`. tsub = corr_cfg.trigger_subsampling tw = cfg.trigger_width rsub = corr_cfg.render_subsampling rw = cfg.render_width # nsamp = orig / subsampling # stride = subsampling * width def calculate_nsamp(width_ms, sub): width_s = width_ms / 1000 return round(width_s * wave.smp_s / sub) trigger_samp = calculate_nsamp(corr_cfg.trigger_ms, tsub) self.render_samp = calculate_nsamp(corr_cfg.render_ms, rsub) self.trigger_stride = tsub * tw self.render_stride = rsub * rw # Create a Trigger object. if isinstance(cfg.trigger, ITriggerConfig): tcfg = cfg.trigger elif isinstance( cfg.trigger, (CommentedMap, dict)): # CommentedMap may/not be subclass of dict. tcfg = attr.evolve(corr_cfg.trigger, **cfg.trigger) elif cfg.trigger is None: tcfg = corr_cfg.trigger else: raise CorrError( f"invalid per-channel trigger {cfg.trigger}, type={type(cfg.trigger)}, " f"must be (*)TriggerConfig, dict, or None") self.trigger = tcfg( wave=self.trigger_wave, tsamp=trigger_samp, stride=self.trigger_stride, fps=corr_cfg.fps, )
def __attrs_post_init__(self) -> None: if not self.nrows: self.nrows = None if not self.ncols: self.ncols = None if self.nrows and self.ncols: raise CorrError("cannot manually assign both nrows and ncols") if not self.nrows and not self.ncols: self.ncols = 1
def flatten(self, flatten: Flatten) -> None: # Reject invalid modes (including Mono). if flatten not in Flatten.modes: # type: ignore # Flatten.Mono not in Flatten.modes. raise CorrError( f"Wave {self.wave_path} has invalid flatten mode {flatten} " f"not in {Flatten.modes}") # If self.is_mono, converts all non-Stereo modes to Mono. self._flatten = flatten if self.is_mono and flatten != Flatten.Stereo: self._flatten = Flatten.Mono
def __init__( self, wave_path: str, amplification: float = 1.0, flatten: Flatten = Flatten.SumAvg, ): self.wave_path = wave_path self.amplification = amplification self.smp_s, self.data = wavfile.read(wave_path, mmap=True) assert self.data.ndim in [1, 2] self.is_mono = self.data.ndim == 1 self.flatten = flatten self.return_channels = False # Cast self.data to stereo (nsamp, nchan) if self.is_mono: self.data.shape = (-1, 1) self.nsamp, stereo_nchan = self.data.shape if stereo_nchan > 2: warnings.warn( f"File {wave_path} has {stereo_nchan} channels, " f"only first 2 will be used", CorrWarning, ) dtype = self.data.dtype # Calculate scaling factor. def is_type(parent: type) -> bool: return np.issubdtype(dtype, parent) # Numpy types: https://docs.scipy.org/doc/numpy/reference/arrays.scalars.html if is_type(np.integer): max_int = np.iinfo(dtype).max + 1 assert max_int & (max_int - 1) == 0 # power of 2 if is_type(np.unsignedinteger): self.center = max_int // 2 self.max_val = max_int // 2 elif is_type(np.signedinteger): self.center = 0 self.max_val = max_int elif is_type(np.floating): self.center = 0 self.max_val = 1 else: raise CorrError(f"unexpected wavfile dtype {dtype}")
def _load_channels(self) -> None: with pushd(self.arg.cfg_dir): # Tell user if master audio path is invalid. # (Otherwise, only ffmpeg uses the value of master_audio) # Windows likes to raise OSError when path contains *, but we don't care. if self.cfg.master_audio and not Path(self.cfg.master_audio).exists(): raise CorrError( f'File not found: master_audio="{self.cfg.master_audio}"' ) self.channels = [Channel(ccfg, self.cfg) for ccfg in self.cfg.channels] self.trigger_waves = [channel.trigger_wave for channel in self.channels] self.render_waves = [channel.render_wave for channel in self.channels] self.triggers = [channel.trigger for channel in self.channels] self.nchan = len(self.channels)
def flatten(self, flatten: FlattenOrStr) -> None: # Reject invalid modes (including Mono). if flatten in _rejected_modes: # Flatten.Mono not in Flatten.modes. raise CorrError( f"Wave {self.wave_path} has invalid flatten mode {flatten} " f"not a numeric string, nor in {Flatten.modes}") # If self.is_mono, converts all non-Stereo modes to Mono. self._flatten = flatten if self.is_mono and flatten != Flatten.Stereo: self._flatten = Flatten.Mono self.flatten_matrix = calc_flatten_matrix(self._flatten, self.stereo_nchan)
def get_trigger(self, index: int, cache: "PerFrameCache") -> int: N = self._buffer_nsamp # Get data data = self._wave.get_around(index, N, self._stride) data -= cache.mean normalize_buffer(data) data *= self._data_window # Window data if cache.period is None: raise CorrError( "Missing 'cache.period', try stacking CorrelationTrigger " "before LocalPostTrigger") # To avoid sign errors, see comment in CorrelationTrigger.get_trigger(). corr = signal.correlate(data, self._windowed_step) assert len(corr) == 2 * N - 1 mid = N - 1 # If we're near a falling edge, don't try to make drastic changes. if corr[mid] < 0: # Give up early. return index # Don't punish negative results too much. # (probably useless. if corr[mid] >= 0, # all other negative entries will never be optimal.) # np.abs(corr, out=corr) # Subtract cost function cost = self._cost_norm / cache.period corr -= cost # Find optimal offset (within ±N/4) mid = N - 1 radius = round(N / 4) left = mid - radius right = mid + radius + 1 corr = corr[left:right] mid = mid - left peak_offset = np.argmax(corr) - mid # type: int trigger = index + (self._stride * peak_offset) return trigger
def render_resolution(self, value: str): error = CorrError(f"invalid resolution {value}, must be WxH") for sep in "x*,": width_height = value.split(sep) if len(width_height) == 2: break else: raise error render = self.cfg.render width, height = width_height try: render.width = int(width) render.height = int(height) except ValueError: raise error
def __init__(self, channels: List[ChannelConfig]): """ Mutates `channels` and `line_color` for convenience. """ super().__init__() self.channels = channels line_color = "line_color" for cfg in self.channels: t = cfg.trigger if isinstance(t, MainTriggerConfig): if not isinstance(t, CorrelationTriggerConfig): raise CorrError(f"Loading per-channel {obj_name(t)} not supported") trigger_dict = attr.asdict(t) else: trigger_dict = dict(t or {}) if line_color in trigger_dict: trigger_dict[line_color] = color2hex(trigger_dict[line_color]) cfg.trigger = trigger_dict
def validate_param(self, key: str, begin: float, end: float) -> None: value = getattr(self, key) if not begin <= value <= end: raise CorrError( f"Invalid {key}={value} (should be within [{begin}, {end}])")
def __init__(self, cfg: ChannelConfig, corr_cfg: "Config", channel_idx: int = 0): """channel_idx counts from 0.""" self.cfg = cfg self.label = cfg.label if not self.label: if corr_cfg.default_label is DefaultLabel.FileName: self.label = Path(cfg.wav_path).stem elif corr_cfg.default_label is DefaultLabel.Number: self.label = str(channel_idx + 1) # Create a Wave object. wave = Wave( abspath(cfg.wav_path), amplification=coalesce(cfg.amplification, corr_cfg.amplification), ) # Flatten wave stereo for trigger and render. tflat = coalesce(cfg.trigger_stereo, corr_cfg.trigger_stereo) rflat = coalesce(cfg.render_stereo, corr_cfg.render_stereo) self.trigger_wave = wave.with_flatten(tflat, return_channels=False) self.render_wave = wave.with_flatten(rflat, return_channels=True) # `subsampling` increases `stride` and decreases `nsamp`. # `width` increases `stride` without changing `nsamp`. tsub = corr_cfg.trigger_subsampling tw = cfg.trigger_width rsub = corr_cfg.render_subsampling rw = cfg.render_width # nsamp = orig / subsampling # stride = subsampling * width def calculate_nsamp(width_ms, sub): width_s = width_ms / 1000 return round(width_s * wave.smp_s / sub) trigger_samp = calculate_nsamp(corr_cfg.trigger_ms, tsub) self._render_samp = calculate_nsamp(corr_cfg.render_ms, rsub) self._trigger_stride = tsub * tw self.render_stride = rsub * rw # Create a Trigger object. if isinstance(cfg.trigger, MainTriggerConfig): tcfg = cfg.trigger elif isinstance( cfg.trigger, (CommentedMap, dict) ): # CommentedMap may/not be subclass of dict. tcfg = evolve_compat(corr_cfg.trigger, **cfg.trigger) elif cfg.trigger is None: tcfg = corr_cfg.trigger else: raise CorrError( f"invalid per-channel trigger {cfg.trigger}, type={type(cfg.trigger)}, " f"must be (*)TriggerConfig, dict, or None" ) self.trigger = tcfg( wave=self.trigger_wave, tsamp=trigger_samp, stride=self._trigger_stride, fps=corr_cfg.fps, wave_idx=channel_idx, )