/ proof-of-concept / cymsim_old
Sign in
proof-of-concept / cymsim_old
cymsim_old / src / cymsim / sound.py
71 lines · 2.8 KBCodeBlameHistory
3688ceacymsim: Python port of cymbalsim with a modal linear model and a PyTorch von Karman modelJeremy Magland 1"""The result of a simulation: the listener pressure, the movie frames, and what the run reported."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from typing import Any
8import numpy as np
10from .params import Listener, Strike
11from .plate import Plate
12from .spectrogram import Spectrogram, spectrogram
13from .wav import Level, apply_level, level_gain, peak_dbfs, write_wav
16@dataclass
17class Sound:
18 pressure: np.ndarray
19 """Listener sound pressure [Pa] at ``fs``, from the moment of impact."""
20 fs: int
21 plate: Plate
22 strike: Strike
23 listener: Listener
24 frames: np.ndarray | None = None
25 """Transverse displacement [m] at ``fps`` frames per second, ``(n_frames, ny, nx)``, or None."""
26 fps: float = 120.0
27 info: dict[str, Any] = field(default_factory=dict)
28 """What the run reported: ``model``, ``peak_force`` [N], ``contact_time`` [s], and more."""
30 @property
31 def duration(self) -> float:
32 return len(self.pressure) / self.fs
34 @property
35 def time(self) -> np.ndarray:
36 """Sample times [s]."""
37 return np.arange(len(self.pressure)) / self.fs
39 @property
40 def peak_spl(self) -> float:
41 """Peak sound pressure level [dB SPL re 20 uPa]."""
42 m = float(np.abs(self.pressure).max()) if len(self.pressure) else 0.0
43 return 20 * np.log10(m / 20e-6) if m > 0 else -np.inf
45 def samples(self, level: Level = "peak", gain_db: float = 0.0) -> np.ndarray:
46 """The signal mapped to full scale in [-1, 1] (see ``cymsim.wav.level_gain``), soft-clipped."""
47 return apply_level(self.pressure, level_gain(self.pressure, self.fs, level, gain_db))
49 def to_wav(self, path, level: Level = "peak", gain_db: float = 0.0) -> None:
50 """Writes a 16-bit mono WAV file. ``level`` is ``"peak"`` (the peak at -1 dBFS),
51 ``"loudness"`` (-20 dBFS RMS) or ``"absolute"`` (1 Pa at 0 dBFS plus ``gain_db``)."""
52 write_wav(path, self.samples(level, gain_db), self.fs)
54 def spectrogram(self, window: int = 2048, hop: int = 256) -> Spectrogram:
55 return spectrogram(self.pressure, self.fs, window, hop)
57 def audio(self, level: Level = "peak", gain_db: float = 0.0):
58 """An ``IPython.display.Audio`` widget for playback in a notebook."""
59 from IPython.display import Audio
61 return Audio(self.samples(level, gain_db), rate=self.fs, normalize=False)
63 def frame_time(self, index: int) -> float:
64 return index / self.fps
66 def __repr__(self) -> str:
67 model = self.info.get("model", "?")
68 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 ")")
70 def peak_dbfs(self, level: Level = "peak", gain_db: float = 0.0) -> float:
71 return peak_dbfs(self.samples(level, gain_db))
↑↓ move↵openescclose