<!-- canonical: https://docs.jmrp.io/phonometry/reference/api/rooms/acoustics/ -->
Source: https://docs.jmrp.io/phonometry/reference/api/rooms/acoustics/

Room acoustic parameters from impulse responses per ISO 3382-1:2009
(performance spaces) and ISO 3382-2:2008 (ordinary rooms).

The measured impulse response (acquired e.g. with the swept-sine or MLS
front end of [`phonometry.room.impulse_response`](/phonometry/reference/api/rooms/impulse-response/), ISO 18233) is filtered into
fractional-octave bands (IEC 61260) and converted to a decay curve by
Schroeder backward integration of the squared impulse response
(ISO 3382-1:2009, 5.3.3, Equation (1)). To limit the influence of
background noise, the integration is truncated at the crossing point
between the background-noise level and a sloping line fitted to the
squared impulse response, and the missing tail is compensated assuming
an exponential decay with the fitted rate (5.3.3, Equation (3)).

From the decay curve the reverberation times are evaluated by
least-squares line fits (ISO 3382-2:2008, Clause 6 and Annex C):
EDT over 0 dB to -10 dB (ISO 3382-1:2009, A.2.2), T20 over -5 dB to
-25 dB and T30 over -5 dB to -35 dB, each extrapolated to a 60 dB decay
(T = -60/slope). The energy parameters follow ISO 3382-1:2009 Annex A:
clarity C50/C80 (Equation (A.10)), definition D50 (Equation (A.11)) and
centre time Ts (Equation (A.13)), with t = 0 at the start of the direct
sound (A.2.1).

Validity flags implement the dynamic-range criterion of ISO 3382-1:2009,
5.3.3: the background noise must lie at least the evaluation range plus
15 dB below the maximum of the (squared) impulse response - 25 dB for EDT
(equivalently, the noise floor sits at least 10 dB below the lowest
evaluation point). The +15 dB rule is derived for finite forward
integration without tail compensation (C = 0), which under-estimates T;
because this module compensates the truncated tail (5.3.3, Equation (3),
C != 0) with a residual positive bias, the T20 and T30 flags add extra
headroom (46 dB for T20, 54 dB for T30) so that a flagged-valid decay
time stays within the 5 % just-noticeable difference of ISO 3382-1:2009,
Table A.1. The curvature indicator C = 100*(T30/T20 - 1) follows
ISO 3382-2:2008, B.3; values above 10 % flag a decay curve that is far
from a straight line.

> Auto-generated from the source docstrings by `scripts/generate_api_docs.py` (`make api-docs`). Do not edit by hand.

## decay_curve

```python
decay_curve(
    ir: Signal | list[float] | np.ndarray,
    fs: int | None = None,
    band: float | None = None,
    fraction: int = 1,
    *,
    zero_phase: bool = False,
) -> DecayCurve
```

Schroeder decay curve of an impulse response.

Backward integration of the squared impulse response
(ISO 3382-1:2009, 5.3.3, Equation (1)), with noise truncation at the
crossing of the background-noise level with the fitted decay slope
and exponential compensation of the missing tail (Equation (3)).
Time zero is the start of the direct sound (A.2.1) and the level is
referenced to the steady-state level (the total energy of the
integrated impulse response, Clause 6).

**Parameters**

| Name | Description |
| :--- | :--- |
| `ir` | Measured impulse response (1D), e.g. from [`phonometry.room.impulse_response`](/phonometry/reference/api/rooms/impulse-response/) (ISO 18233). Accepts a [`phonometry.io.Signal`](/phonometry/reference/api/io/io/#signal), whose calibration is applied to the samples and then cancels: the curve is normalised to 0 dB at its start, so a factor on the record moves neither the levels nor the decay times read off them. |
| `fs` | Sample rate in Hz. Required for a bare array; a [`Signal`](/phonometry/reference/api/io/io/#signal) brings its own, and an explicit value that disagrees with it raises instead of silently winning. |
| `band` | Optional band centre frequency in Hz. When given, the impulse response is first filtered with the matching IEC 61260 fractional-octave filter; when None the broadband response is integrated directly. |
| `fraction` | Bandwidth fraction of the band filter (1 = octave, 3 = one-third octave). Only used when `band` is not None. |
| `zero_phase` | If True, filter the band with forward-backward (zero-phase) filtering, removing the octave filter's group delay before the backward integration. ISO 3382-2:2008 Clause 7.3 NOTE permits time-reversed filtering (it relaxes the B\*T > 16 rule to B\*T > 4); it roughly halves the low-frequency short-decay bias at 125 Hz. Only used when `band` is not None. Default False (causal). |

**Returns:** A [`DecayCurve`](/phonometry/reference/api/rooms/acoustics/#decaycurve) with `times` in seconds from the direct sound and `levels` in dB (0 dB at time zero), up to the noise truncation point. It unpacks as `times, levels = decay_curve(...)` for backward compatibility and exposes [`DecayCurve.plot`](/phonometry/reference/api/rooms/acoustics/#decaycurveplot).

## DECAY_UNCERTAINTY_COEFFICIENTS

*Constant* (`mapping`).

```python
DECAY_UNCERTAINTY_COEFFICIENTS = {20.0: (0.88, 1.9), 30.0: (0.55, 1.52)}
```

## DecayCurve

```python
DecayCurve(times: np.ndarray, levels: np.ndarray, band: float | None = None)
```

Schroeder backward-integrated decay curve of an impulse response.

`times` holds the sample times in seconds from the direct sound and
`levels` the decay levels in dB (0 dB at time zero), up to the noise
truncation point (ISO 3382-1:2009, 5.3.3). `band` is the
octave/third-octave band centre in Hz, or `None` for a broadband decay.

For backward compatibility with the previous `(time, level)` tuple
return of [`decay_curve`](/phonometry/reference/api/rooms/acoustics/#decay_curve), the dataclass is iterable and unpacks as
`time, level = decay_curve(...)`.

### DecayCurve.plot()

```python
DecayCurve.plot(
    ax: Axes | None = None,
    *,
    language: str = 'en',
    **kwargs: Any,
) -> Axes
```

Plot the decay curve with optional straight T-fit overlays.

Requires matplotlib (`pip install phonometry[plot]`); returns the
`Axes`. Pass `fits=False` to omit the
EDT/T20/T30 fit lines.

## filter_bandwidth

```python
filter_bandwidth(centre: ArrayLike, fraction: int = 1) -> np.ndarray | float
```

Bandwidth of a fractional-octave filter (ISO 3382-1:2009, 7.1).

The `B` of Equations (4) to (6), as the clause prints it: 0,71 times
the mid-band frequency for an octave filter and 0,23 times it for a
one-third-octave one. See [`FILTER_BANDWIDTH_FRACTION`](/phonometry/reference/api/rooms/acoustics/#filter_bandwidth_fraction) for how
those two figures sit against the exact IEC 61260 band edges.

**Parameters**

| Name | Description |
| :--- | :--- |
| `centre` | Mid-band frequency, in Hz. |
| `fraction` | Bandwidth fraction: 1 for an octave filter, 3 for a one-third-octave one. |

**Returns:** The bandwidth in Hz, in the shape of `centre`.

**Raises**

| Exception | When |
| :--- | :--- |
| ValueError | If `fraction` is not one the clause prints a coefficient for, or a centre frequency is not positive and finite. |

## FILTER_BANDWIDTH_FRACTION

*Constant* (`mapping`).

```python
FILTER_BANDWIDTH_FRACTION = {1: 0.71, 3: 0.23}
```

## INTEGRATED_RESPONSE_DECAYS

*Constant* (`int`).

```python
INTEGRATED_RESPONSE_DECAYS = 10
```

## MINIMUM_BANDWIDTH_TIME_PRODUCT

*Constant* (`float`).

```python
MINIMUM_BANDWIDTH_TIME_PRODUCT = 16.0
```

## MINIMUM_DETECTOR_MULTIPLE

*Constant* (`float`).

```python
MINIMUM_DETECTOR_MULTIPLE = 2.0
```

## minimum_reliable_reverberation_time

```python
minimum_reliable_reverberation_time(
    bandwidth: ArrayLike,
    detector_time: float = 0.0,
) -> np.ndarray | float
```

Shortest decay time a forward analysis can be trusted with (7.3).

ISO 3382-1:2009, Equations (6) and (7) put two lower limits on a
reverberation time measured by traditional forward analysis, and both
are normative:

$$
B T > 16, \qquad T > 2\, T_\mathrm{det}
$$

The first is the filter's: a band of width `B` cannot resolve a decay
faster than its own impulse response. The second is the averaging
detector's, and it drops out when the analysis has no detector, which is
the case for the backward integration of 5.3.3. This function returns the
larger of the two, which is the limit that binds. Both relations are
strict, so the value returned is a bound the decay time has to clear and
not one it may equal: a room whose decay time is exactly this long is
already outside what a forward analysis can be trusted with.

ISO 3382-2:2008, 7.3 NOTE relaxes the first to `B T > 4` when the
filtering is time-reversed, which is what
[`phonometry.room.room_parameters`](/phonometry/reference/api/rooms/acoustics/#room_parameters) does with `zero_phase=True`.

**Parameters**

| Name | Description |
| :--- | :--- |
| `bandwidth` | Bandwidth of the analysis filter, in Hz; [`filter_bandwidth`](/phonometry/reference/api/rooms/acoustics/#filter_bandwidth) gives the clause's own figure for it. |
| `detector_time` | Reverberation time of the averaging detector, in seconds. Zero, the default, for an analysis with no detector. |

**Returns:** The exclusive lower bound on a reliable reverberation time, in seconds, of the shape of `bandwidth`.

**Raises**

| Exception | When |
| :--- | :--- |
| ValueError | If a bandwidth is not positive and finite, or the detector time is not a finite time of zero seconds or more. |

## reverberation_time_standard_deviation

```python
reverberation_time_standard_deviation(
    reverberation_time: ArrayLike,
    bandwidth: ArrayLike,
    *,
    evaluation_range: float = 30.0,
    decays: ArrayLike = 10,
    positions: ArrayLike = 1,
) -> np.ndarray | float
```

Standard deviation of a measured reverberation time (7.1).

ISO 3382-1:2009, Equations (4) and (5):

$$
\sigma(T_{20}) = 0{,}88\, T_{20} \sqrt{\frac{1 + 1{,}90/n}{N B T_{20}}}, \qquad \sigma(T_{30}) = 0{,}55\, T_{30} \sqrt{\frac{1 + 1{,}52/n}{N B T_{30}}}
$$

The uncertainty is a property of the *excitation*, not of the room: the
interrupted-noise method restarts a random process for every decay, and
the clause quantifies how much of the answer that randomness owns. The
integrated impulse response method is deterministic, and 7.2 values it at
ten interrupted-noise decays per position rather than at the infinity the
theory gives, which is what `decays` defaults to.

Note that $\sigma$ grows as $\sqrt{T}$, not as $T$: the
prefactor's $T$ and the $T$ under the radical leave one half
power between them, so a long reverberation time is measured with a
larger absolute uncertainty and a smaller relative one.

**Parameters**

| Name | Description |
| :--- | :--- |
| `reverberation_time` | The measured decay time, in seconds. |
| `bandwidth` | Bandwidth of the analysis filter, in Hz; [`filter_bandwidth`](/phonometry/reference/api/rooms/acoustics/#filter_bandwidth) gives the clause's own figure for it. |
| `evaluation_range` | The range the decay time was fitted over, in dB: 20 for $T_{20}$ or 30 for $T_{30}$, the two the clause prints coefficients for. |
| `decays` | Decays measured in each position, the `n` of the equations. Broadcast against the rest. |
| `positions` | Independent measurement positions, the `N`: source and receiver combinations, not receivers alone. Broadcast against the rest, so a sweep over survey sizes is one call. |

**Returns:** The standard deviation in seconds, of the broadcast shape.

**Raises**

| Exception | When |
| :--- | :--- |
| ValueError | If `evaluation_range` is not one the clause prints coefficients for, if `decays` or `positions` is below one, or if a reverberation time or bandwidth is not positive and finite. An infinite `decays` is taken: it is the limit 7.2 declines to use. |

## room_parameters

```python
room_parameters(
    ir: Signal | list[float] | np.ndarray,
    fs: int | None = None,
    limits: tuple[float, float] | None = (125.0, 4000.0),
    fraction: int = 1,
    *,
    zero_phase: bool = False,
) -> RoomAcousticsResult
```

Room acoustic parameters per ISO 3382-1:2009 / ISO 3382-2:2008.

The impulse response (e.g. acquired with the ISO 18233 swept-sine or
MLS methods of [`phonometry.room.impulse_response`](/phonometry/reference/api/rooms/impulse-response/)) is filtered into
fractional-octave bands (IEC 61260) and each band decay curve is
obtained by Schroeder backward integration with noise truncation and
tail compensation (ISO 3382-1:2009, 5.3.3). Least-squares line fits
(ISO 3382-2:2008, Annex C) yield EDT (0 dB to -10 dB, ISO 3382-1,
A.2.2), T20 (-5 dB to -25 dB) and T30 (-5 dB to -35 dB), each
extrapolated to 60 dB. Clarity C50/C80, definition D50 and centre
time Ts follow ISO 3382-1:2009, Equations (A.10), (A.11) and (A.13),
with t = 0 at the start of the direct sound.

Values that cannot be evaluated (evaluation range unreachable, or
reaching below the noise floor + 10 dB) are NaN. The validity flags
apply the dynamic-range criterion of ISO 3382-1:2009, 5.3.3 (noise
at least evaluation range + 15 dB below the maximum of the impulse
response: 25 dB for EDT), with T20 and T30 raised to 46 dB and 54 dB
to absorb the positive bias of the tail compensation and keep a
flagged-valid decay time within the 5 % JND (ISO 3382-1:2009,
Table A.1).

**Parameters**

| Name | Description |
| :--- | :--- |
| `ir` | Measured impulse response (1D). Accepts a [`phonometry.io.Signal`](/phonometry/reference/api/io/io/#signal), whose calibration is applied to the samples and then cancels: every parameter here is a decay time, a ratio of energies or a centre time, and none of them moves with a factor on the record. |
| `fs` | Sample rate in Hz. Required for a bare array; a [`Signal`](/phonometry/reference/api/io/io/#signal) brings its own, and an explicit value that disagrees with it raises instead of silently winning. |
| `limits` | `(f_min, f_max)` band-centre limits in Hz; default octave bands 125 Hz to 4 kHz (ISO 3382-1:2009, 5.1). Use `(100.0, 5000.0)` with `fraction=3` for the one-third-octave engineering/precision range. `None` analyses the broadband response as a single band (`frequencies` is then `None`). |
| `fraction` | Bandwidth fraction (1 = octave, 3 = one-third octave). Default 1. |
| `zero_phase` | If True, use forward-backward (zero-phase) octave filtering, removing the filter group delay before the backward integration. ISO 3382-2:2008 Clause 7.3 NOTE permits time-reversed filtering (relaxing B\*T > 16 to B\*T > 4); it roughly halves the 125 Hz short-decay T30 bias (about +4.9 % -> +2.4 % at T = 0.2 s). The benefit is small next to the ~10 % measurement variance but is free and standards-sanctioned. Default False (causal filtering). |

**Returns:** [`RoomAcousticsResult`](/phonometry/reference/api/rooms/acoustics/#roomacousticsresult) with one entry per band.

## RoomAcousticsResult

```python
RoomAcousticsResult(
    frequencies: np.ndarray | None,
    edt: np.ndarray,
    t20: np.ndarray,
    t30: np.ndarray,
    c50: np.ndarray,
    c80: np.ndarray,
    d50: np.ndarray,
    ts: np.ndarray,
    dynamic_range: np.ndarray,
    curvature: np.ndarray,
)
```

Per-band room acoustic parameters from one impulse response.

All arrays have one entry per analysis band (`frequencies` holds the
exact band centre frequencies; it is `None` for a broadband
analysis, in which case the arrays have length 1). `edt`, `t20`
and `t30` are decay times in seconds extrapolated to 60 dB
(ISO 3382-1:2009, A.2.2; ISO 3382-2:2008, Clause 6); `c50`/`c80`
are early-to-late indices in dB (Equation (A.10)), `d50` the
definition ratio (Equation (A.11)) and `ts` the centre time in
seconds (Equation (A.13); the Table A.1 JND is 10 ms).

`dynamic_range` is the peak-to-noise-floor distance of the squared
band impulse response in dB. `edt_valid`, `t20_valid` and
`t30_valid` are read from it and the decay times, so they are not
fields; they apply the ISO 3382-1:2009, 5.3.3 criterion (noise at
least evaluation range + 15 dB below the maximum: 25 dB for EDT), with
T20 and T30 tightened to 46 dB and 54 dB to absorb the positive bias of
the tail compensation (5.3.3, Eq. (3)) and keep a flagged-valid value
within the 5 % JND (ISO 3382-1:2009, Table A.1); they are False when the
value could not be evaluated. `curvature` is
C = 100*(T30/T20 - 1) in percent (ISO 3382-2:2008, B.3); values
above 10 % indicate an unreliable, non-straight decay.

### RoomAcousticsResult.edt_valid

*property*

Per band, whether the EDT is evaluated with the noise far enough below.

At least 10 dB + 15 dB under the maximum (ISO 3382-1:2009, 5.3.3);
`False` where the value could not be evaluated.

### RoomAcousticsResult.plot()

```python
RoomAcousticsResult.plot(
    ax: Axes | None = None,
    *,
    language: str = 'en',
    **kwargs: Any,
) -> Axes | np.ndarray
```

Plot per-band decay times (EDT/T20/T30) and clarity (C50/C80).

Invalid bands are hatched and greyed. With `ax` given, only the
decay-times panel is drawn on it. Requires matplotlib
(`pip install phonometry[plot]`); returns the
`Axes` (or array thereof).

### RoomAcousticsResult.report()

```python
RoomAcousticsResult.report(
    path: str,
    *,
    metadata: ReportMetadata | None = None,
    engine: str = 'reportlab',
    verbose: bool = False,
    language: str = 'en',
) -> str
```

Render a room acoustic parameters fiche to a PDF (ISO 3382-1/-2).

Writes a one-page report laid out like a room-acoustics measurement
report: the standard-basis line, an optional metadata header block
(room, volume, source/receiver positions, climate ...), the full-width
per-band parameter table (T20/T30/EDT and C50/C80/D50/Ts) above the
result's own per-band decay-time plot (`plot`), the boxed
mid-frequency reverberation time T_mid (the mean of the 500 Hz and
1000 Hz band T30; a one-third-octave analysis averages the 500 Hz and
1 kHz one-third-octave bands and labels them as such), an optional
verdict row and a footer with the fixed disclaimer. ISO 3382-1/-2 are
characterisation standards with no intrinsic pass/fail, so the verdict
row appears only when a target T is supplied through
`metadata.requirement` (read as the maximum acceptable value of
whichever descriptor the box carries). A broadband result
(`frequencies` is `None`) has no 500 Hz and 1000 Hz bands to average,
so the box and the verdict fall back to the plain broadband T30 instead
of a mid-frequency average, with no "500-1000 Hz" label; so does a
banded result that does not span both mid bands, or that spans them
with a NaN T30 in either, which box and compare the first finite T30
band (the box names that band, the verdict line does not).

**Parameters**

| Name | Description |
| :--- | :--- |
| `path` | Destination path of the PDF file. |
| `metadata` | Optional [`ReportMetadata`](/phonometry/reference/api/building/insulation/#reportmetadata); `None` produces a bare characterisation fiche (body, result and disclaimer only). The room-specific fields `room_volume`, `source_positions` and `receiver_positions` populate the header; `requirement` is read as the maximum acceptable reverberation time, compared with the descriptor the result box shows (the mid-frequency T where both mid bands carry a finite T30, otherwise a single band's T30). |
| `engine` | Rendering back end; only `"reportlab"` is supported. |
| `verbose` | Accepted for parity with the other fiches; the room table already shows every computed parameter, so it has no effect. |
| `language` | Fiche language: `"en"` (default, English) or `"es"` (Spanish, with a comma decimal separator). |

**Returns:** The written `path` as a `str`.

**Raises**

| Exception | When |
| :--- | :--- |
| ValueError | If `engine` is not `"reportlab"`. |
| ImportError | If reportlab is not installed (`pip install phonometry[report]`), or matplotlib is missing for the embedded figure (`pip install phonometry[plot]`). |

### RoomAcousticsResult.t20_valid

*property*

Per band, whether T20 is evaluated with the noise far enough below.

20 dB + 15 dB, tightened by the tail headroom to 46 dB so a
flagged-valid value stays within the 5 % JND; `False` where the
value could not be evaluated.

### RoomAcousticsResult.t30_valid

*property*

Per band, whether T30 is evaluated with the noise far enough below.

30 dB + 15 dB, tightened by the tail headroom to 54 dB so a
flagged-valid value stays within the 5 % JND; `False` where the
value could not be evaluated.
