"""The result of a simulation: the listener pressure, the movie frames, and what the run reported.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any import numpy as np from .params import Listener, Strike from .plate import Plate from .spectrogram import Spectrogram, spectrogram from .wav import Level, apply_level, level_gain, peak_dbfs, write_wav @dataclass class Sound: pressure: np.ndarray """Listener sound pressure [Pa] at ``fs``, from the moment of impact.""" fs: int plate: Plate strike: Strike listener: Listener frames: np.ndarray | None = None """Transverse displacement [m] at ``fps`` frames per second, ``(n_frames, ny, nx)``, or None.""" fps: float = 120.0 info: dict[str, Any] = field(default_factory=dict) """What the run reported: ``model``, ``peak_force`` [N], ``contact_time`` [s], and more.""" @property def duration(self) -> float: return len(self.pressure) / self.fs @property def time(self) -> np.ndarray: """Sample times [s].""" return np.arange(len(self.pressure)) / self.fs @property def peak_spl(self) -> float: """Peak sound pressure level [dB SPL re 20 uPa].""" m = float(np.abs(self.pressure).max()) if len(self.pressure) else 0.0 return 20 * np.log10(m / 20e-6) if m > 0 else -np.inf def samples(self, level: Level = "peak", gain_db: float = 0.0) -> np.ndarray: """The signal mapped to full scale in [-1, 1] (see ``cymsim.wav.level_gain``), soft-clipped.""" return apply_level(self.pressure, level_gain(self.pressure, self.fs, level, gain_db)) def to_wav(self, path, level: Level = "peak", gain_db: float = 0.0) -> None: """Writes a 16-bit mono WAV file. ``level`` is ``"peak"`` (the peak at -1 dBFS), ``"loudness"`` (-20 dBFS RMS) or ``"absolute"`` (1 Pa at 0 dBFS plus ``gain_db``).""" write_wav(path, self.samples(level, gain_db), self.fs) def spectrogram(self, window: int = 2048, hop: int = 256) -> Spectrogram: return spectrogram(self.pressure, self.fs, window, hop) def audio(self, level: Level = "peak", gain_db: float = 0.0): """An ``IPython.display.Audio`` widget for playback in a notebook.""" from IPython.display import Audio return Audio(self.samples(level, gain_db), rate=self.fs, normalize=False) def frame_time(self, index: int) -> float: return index / self.fps def __repr__(self) -> str: model = self.info.get("model", "?") return f"Sound({model} model, {self.duration:.2f} s at {self.fs} Hz, peak {self.peak_spl:.0f} dB SPL" + (f", {len(self.frames)} frames)" if self.frames is not None else ")") def peak_dbfs(self, level: Level = "peak", gain_db: float = 0.0) -> float: return peak_dbfs(self.samples(level, gain_db))