Speech Transmission Index (STI)
Standards: IEC 60268Key references: Houtgast & Steeneken 1985
A public-address system, an intercom, a reverberant lecture hall: each is a transmission channel between a talker’s mouth and a listener’s ear, and each degrades speech in its own way. The Speech Transmission Index (STI) of IEC 60268-16 rates that channel with a single number in by measuring how much of the speech envelope survives the trip. This page covers the modulation-transfer physics behind the index, the indirect method from a measured room impulse response, and the direct STIPA measurement with its standardized test signal.
How do I compute the IEC 60268-16 Speech Transmission Index in Python?
Section titled “How do I compute the IEC 60268-16 Speech Transmission Index in Python?”From a measured room impulse response, call
speech.sti_from_impulse_response(ir, fs, snr=25.0). The result gives sti
on the 0 to 1 scale, its Annex F rating letter and the seven octave-band
modulation transfer indices. For a direct measurement, play
speech.stipa_signal(fs) in the room and pass the recording to
speech.stipa(recording, fs).
1. The modulation transfer function
Section titled “1. The modulation transfer function”Reverberation and noise do not muffle speech uniformly; they blur its envelope: the slow (0.63–12.5 Hz) intensity modulations that carry syllables. STI quantifies how much of that modulation survives from mouth to ear, per octave band, as the modulation transfer function . A delta-like channel keeps (STI = 1); reverberation low-passes the envelope following Schroeder’s closed form, and steady noise scales it:
Modulation depth is the thing worth measuring because intelligibility rides on the depth of the envelope valleys, not on the loudness of the peaks. A talker alternates energy bursts (vowels) with near-silences (stop gaps, fricative onsets) at syllable rate, and a listener segments speech by hearing those dips. A reverberant tail fills the dips from behind, since late energy smears into the gaps; steady noise raises their floor. In both cases the received modulation depth shrinks, and with it the contrast between speech sounds, even when the average level barely changes. The full method probes at 14 modulation frequencies (0.63 Hz to 12.5 Hz in one-third-octave steps) in each of the 7 octave bands from 125 Hz to 8 kHz, converts each to an effective signal-to-noise ratio clipped to ±15 dB, and combines the results, band-weighted, into the index: the STI is an effective SNR of the envelope, mapped onto .
That is a claim about a waveform, so it is worth watching on one. The clip below sends a fully modulated 4 Hz envelope — one syllable-rate burst and gap per quarter second — through the 1 kHz octave band of a room, and lets first the reverberation time and then the noise take it apart. The received trace is the probe convolved with the very the Schroeder integral above runs on, so the depth you can measure off the screen is the the library returns. Both halves are drawn at a constant received mean, which is the whole point: a sound level meter pointed at any frame of this clip reads the same number, and the index does not.
A fully modulated four-hertz intensity envelope is received through the one-kilohertz octave band of a room. As the reverberation time sweeps from 0.30 to 2.50 seconds the peaks fall and the valleys fill toward a mean line that never moves, and the measured modulation depth falls from 0.90 to 0.26; the modulation transfer curve beside it drops at every one of the fourteen modulation frequencies, the seven band modulation transfer indices fall with it and the speech transmission index walks from 0.84 down to 0.40. Then, at a fixed one-second reverberation time, the speech-to-noise ratio sweeps from 25 to 0 decibels: a shaded noise floor rises under the trace instead, the mean still does not move, and the depth falls again to 0.28 for a speech transmission index of 0.36.
A fully modulated four-hertz intensity envelope is received through the one-kilohertz octave band of a room. As the reverberation time sweeps from 0.30 to 2.50 seconds the peaks fall and the valleys fill toward a mean line that never moves, and the measured modulation depth falls from 0.90 to 0.26; the modulation transfer curve beside it drops at every one of the fourteen modulation frequencies, the seven band modulation transfer indices fall with it and the speech transmission index walks from 0.84 down to 0.40. Then, at a fixed one-second reverberation time, the speech-to-noise ratio sweeps from 25 to 0 decibels: a shaded noise floor rises under the trace instead, the mean still does not move, and the depth falls again to 0.28 for a speech transmission index of 0.36.
The two degradations look nothing alike on that curve, which is why the index needs the whole rather than one number:
Reverberation and noise degrade the same quantity in two distinguishable ways. A decay is a low-pass filter on the envelope, and lengthening it moves the corner down (the dashed lines are the closed form above, which the measured points follow closely). Steady noise instead multiplies the whole curve by , which is flat in — at 0 dB that factor is exactly one half. A single modulation frequency cannot tell the two apart, which is why the full method probes fourteen of them and STIPA still probes two per band (marked on the left panel for this band).
Show the code for this figure
import matplotlib.pyplot as pltimport numpy as npfrom phonometry import speech
fs = 48000rng = np.random.default_rng(0)
# The 14 full-STI modulation frequencies (0.63 Hz to 12.5 Hz, third-octave).MOD_FREQS = np.array([0.63, 0.80, 1.00, 1.25, 1.60, 2.00, 2.50, 3.15, 4.00, 5.00, 6.30, 8.00, 10.0, 12.5])
def decay(t60): n = np.arange(int(2.5 * t60 * fs)) return rng.standard_normal(n.size) * np.exp(-6.9078 * n / fs / t60)
fig, (ax_t, ax_n) = plt.subplots(1, 2, figsize=(12.6, 5.2))for t60 in (0.3, 0.9, 2.5): mtf = speech.sti_from_impulse_response(decay(t60), fs).mtf[3] # 1 kHz ax_t.semilogx(MOD_FREQS, mtf, "o-", label=f"T60 = {t60} s") closed = 1 / np.sqrt(1 + (2 * np.pi * MOD_FREQS * t60 / 13.8) ** 2) ax_t.semilogx(MOD_FREQS, closed, "--")
ir = decay(0.9)for snr in (20.0, 10.0, 0.0): mtf = speech.sti_from_impulse_response(ir, fs, snr=snr).mtf[3] ax_n.semilogx(MOD_FREQS, mtf, "o-", label=f"SNR = {snr:g} dB")ax_t.legend(); ax_n.legend()plt.show()The markers are full pipeline runs of sti_from_impulse_response on
synthesized decays; the dashed line is the analytic Schroeder prediction, so
their agreement is the indirect method’s own sanity check. The shaded ladder
down the right margin is the Annex F qualification scale from U to A+, the
rating letter the result returns, so the curve reads directly as the class a
room of that reverberation time can reach.
Show the code for this figure
# Both features of the figure: the measured points against the closed form,# and the Annex F ladder they are read against.t60_points = [0.3, 0.5, 0.8, 1.2, 2.0, 3.0, 5.0]alpha = np.array([0.085, 0.127, 0.230, 0.233, 0.309, 0.224, 0.173]) # Ed.5 malebeta = np.array([0.085, 0.078, 0.065, 0.011, 0.047, 0.095])
def analytic_sti(t60): m = 1 / np.sqrt(1 + (2 * np.pi * MOD_FREQS * t60 / 13.8) ** 2) snr_eff = np.clip(10 * np.log10(m / (1 - m)), -15.0, 15.0) # the ±15 dB clip mti = np.full(7, ((snr_eff + 15.0) / 30.0).mean()) return float(alpha @ mti - beta @ np.sqrt(mti[:-1] * mti[1:]))
measured = [speech.sti_from_impulse_response(decay(t), fs).sti for t in t60_points]print(np.round(measured, 3)) # [0.827 0.731 0.641 0.555 0.437 0.368 0.269]
fig, ax = plt.subplots(figsize=(10, 6))edges = [0.36, 0.40, 0.44, 0.48, 0.52, 0.56, 0.60, 0.64, 0.68, 0.72, 0.76]letters = ["U", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A", "A+"]for lo, hi, letter in zip([0.15, *edges], [*edges, 0.95], letters): ax.axhspan(lo, hi, color=plt.get_cmap("RdYlGn")(letters.index(letter) / 11), alpha=0.18, lw=0, zorder=0) ax.text(1.005, (lo + hi) / 2, letter, transform=ax.get_yaxis_transform(), va="center", fontsize=8)dense = np.logspace(np.log10(0.25), np.log10(6.0), 200)ax.semilogx(dense, [analytic_sti(t) for t in dense], "--")ax.semilogx(t60_points, measured, "o")ax.set_xlabel("Reverberation time T60 [s]")ax.set_ylabel("STI")ax.set_ylim(0.15, 0.95)plt.show()2. Indirect and direct (STIPA) measurement
Section titled “2. Indirect and direct (STIPA) measurement”Setting up the measurement
Section titled “Setting up the measurement”Both routes below start from a signal that travelled a real path, and clause 7 is specific about what produces it. The page covers two physically different measurements — an unamplified talker in a room, and a sound system driven electrically — and the setup differs mainly in where the signal enters.
The source. For an unamplified talker, use an artificial mouth or mouth simulator with head-and-mouth directivity (the standard points at ITU-T P.51), because in a listening space intelligibility depends on the source directivity. In its absence, a small single-source high-quality loudspeaker with a cone diameter not exceeding 100 mm may be used, and it shall be described with the results. Verify that the source’s one-third-octave frequency response is within ±1 dB over the range the chosen signal needs — 88 Hz to 11.3 kHz for a full-STI or impulse-response signal, or octave band by octave band from 125 Hz to 8 kHz for STIPA — measured in a free field, and equalize it if it is not. Then set it on the axis of the microphone at the real talker position and distance, pointing in the normal speaking direction (clause 7.2 a to c).
The level. Match the operational speech level with the Annex J procedure. Where that match is not available, the standard’s fallback is an equivalent level of 60 dB(A) at 1 m in front of the artificial mouth or test loudspeaker. This is not a detail you can guess from the room: a close-talking microphone sees a speech level of about 86 dB(A) to 94 dB(A) at 5 cm to 2 cm, and a gooseneck about 80 dB(A) to 86 dB(A) at 10 cm to 5 cm (clause 7.2 d). For a sound system the signal is injected electrically instead, as close to the normal input as possible so that every equalizer, delay and processor in the chain is included, and adjusted to the level of speech at that point by the same Annex J method (clause 7.4).
The receiver. The measurement device — microphone, artificial ear or head
simulator — shall be acoustically calibrated for sensitivity and frequency
response, and the measurement made at the listener’s normal location and
listening height (about 1.2 m seated, 1.6 m standing). A single microphone
shall be omnidirectional and of diffuse-field type; a directional microphone
gives results that do not correlate with the STI model and is not advised
(clauses 4.1 and 7.3). For headsets, use an in-ear microphone or an artificial
ear. Measure the ambient noise at the same point with the source switched off,
so it can be entered through ambient= or as snr=.
Before blaming the room, verify the integrity of the test signal with a loop-back measurement — this catches a corrupted or over-compressed file, and the standard advises against digitally compressed formats, noting that schemes of at least 128 kbit/s have been shown to work (clause 7.2 a).
import numpy as npfrom phonometry import speech
fs = 48000# A measured room impulse response (synthesized decay so the example runs)ir = np.random.default_rng(0).standard_normal(fs) * np.exp(-6.9 * np.arange(fs) / fs / 0.5)
# Indirect method: from a measured room impulse responseres = speech.sti_from_impulse_response(ir, fs, snr=25.0)print(f"STI = {res.sti:.2f} ({res.rating})") # 0.73 (A)
# Direct STIPA measurement: play speech.stipa_signal() in the room, record ittest = speech.stipa_signal(fs, seconds=18.0, level_db=80.0)recording = test # in practice, the microphone signal after playbackres = speech.stipa(recording, fs)res.plot() # per-band modulation transfer index (MTI) bars, STI + rating in the titleWhere snr= comes from. It is the second input of the indirect method and
it carries the whole noise degradation, so its provenance matters as much as
the impulse response’s. For each octave band it is the band level of the speech
signal at the listener position minus the band level of the ambient noise
measured at that same position with the source switched off — which is why the
argument accepts a 7-vector: real ambient noise is rarely flat, and ventilation
noise concentrated at 125 Hz and 250 Hz costs the low bands far more than a
single broadband figure suggests. With snr=None the calculation assumes a
noise-free channel, so the result is the room’s own limit and an upper bound
on what any listener will experience; quoting it as a measured STI is exactly
what makes a hall look acceptable on paper. Measure the ambient spectrum once,
measure or estimate the speech spectrum at the position, and pass the
band-by-band difference — or, when the absolute levels matter because auditory
masking and the reception threshold are in play, pass level= and ambient=
instead so the level-dependent stages of the standard are applied rather than a
pure ratio.
Whichever route produced it, the result is worth reading band by band before the single number is quoted: the STI is a weighted combination of seven octave-band modulation transfer indices, and a room usually fails in a particular part of the spectrum rather than uniformly.
Show the code for this figure
import numpy as npimport matplotlib.pyplot as pltfrom phonometry import speech
# A reverberant hall (T60 = 0.9 s) measured with a 15 dB speech-to-noise# ratio: a synthesized exponential decay stands in for the measured IR.fs = 48000rng = np.random.default_rng(0)n = np.arange(fs)ir = rng.standard_normal(fs) * np.exp(-6.9078 * n / fs / 0.9)res = speech.sti_from_impulse_response(ir, fs, snr=15.0)print(round(res.sti, 3), res.rating) # 0.583 E
# One line: the per-band MTI bars with the STI and its rating in the title.res.plot()plt.show()
# By hand, mirroring what STIResult.plot() draws:bands = [125, 250, 500, 1000, 2000, 4000, 8000]fig, ax = plt.subplots()ax.bar(np.arange(len(bands)), res.mti)ax.set_xticks(np.arange(len(bands)))ax.set_xticklabels([f"{b}" for b in bands])ax.set_xlabel("Frequency [Hz]")ax.set_ylabel("Modulation transfer index MTI")ax.set_ylim(0.0, 1.0)ax.set_title(f"STI = {res.sti:.2f} (rating {res.rating})")plt.show()In this hall the seven indices sit within 0.06 of each other, the signature of a decay that is uniform across the spectrum plus a broadband noise floor. A profile that sags at 125 Hz and 250 Hz instead points at low-frequency reverberation (too little bass absorption), while one that falls only at 4 kHz and 8 kHz usually means the loudspeaker is out of the listener’s direct-sound coverage, since air and directivity strip the top bands first. Those are different remedies, and only the per-band view distinguishes them.
The direct measurement sends the STIPA signal along the full chain drawn below, from the source through the room to the microphone and into the per-band modulation analysis that yields the index.
stipa emits a UserWarning when the recording is shorter than the
recommended 15 s (IEC 60268-16 STIPA practice, 15 s to 25 s): below that the
slow modulation components are averaged over too few periods and the STI is
biased low (an ideal loopback gives STI ≈ 0.956 at 5 s vs ≈ 0.998 at 18 s).
The implementation follows Edition 5 (2020): Edition 4’s normative PDF is the base and every Ed. 5 change is source-attributed in the code; the only numeric delta is the revised male speech spectrum of clause A.6.1. CI checks the standard’s own verification vectors: the six weighting-factor band pairs to ±0.001 STI, the ↔ STI mapping table, the level-dependent masking control points, and Schroeder-form decays at four values.
The analyzer is also verified end to end against the IEC 60268-16 rev 5 verification test bench signals from stipa.info (Embedded Acoustics BV): the direct-method modulation-depth staircase (Annex C.3.2), the indirect-method exponential decays against the closed-form Schroeder MTF (C.3.3), the filter-bank slope test with a +41 dB unmodulated adjacent-octave tone (C.4.2, ), the weighting-factor band pairs (A.2.2) and the filter-bank phase-distortion test with half-octave edge carriers (A.3.1.2, |STI bias| < 0.01 over TI = 0.1–0.9). All five suites pass with the level-dependent features disabled, as the bench prescribes. The 49 certified WAVs stay local (third-party data, not committed); CI re-derives the same signal constructions synthetically in the conformance suite.
Reading the result: the Annex F bands
Section titled “Reading the result: the Annex F bands”Every example on this page ends in a letter, and the letter is the part a
client reads. Annex F divides the scale into bands with edges at 0.36, 0.40,
0.44 … 0.76, and Annex G Table G.1 gives an example of what each band is used
for. STIResult.rating returns the letter; the nominal STI value below is the
centre of the band:
| Rating | STI range | Nominal | Typical use (Annex G Table G.1) |
|---|---|---|---|
A+ | ≥ 0.76 | — | Recording studios; excellent but rarely achievable |
A | 0.72–0.76 | 0.74 | Theatres, speech auditoria, parliaments, courts, assistive hearing systems |
B | 0.68–0.72 | 0.70 | Theatres, speech auditoria, teleconferencing |
C | 0.64–0.68 | 0.66 | Complex messages with unfamiliar words |
D | 0.60–0.64 | 0.62 | Lecture theatres, classrooms, concert halls |
E | 0.56–0.60 | 0.58 | Concert halls, modern churches; high-quality PA |
F | 0.52–0.56 | 0.54 | PA in shopping malls and public buildings, VA systems, cathedrals |
G | 0.48–0.52 | 0.50 | Target value for voice-alarm systems |
H | 0.44–0.48 | 0.46 | VA and PA in difficult acoustic environments; normal lower limit for VA |
I | 0.40–0.44 | 0.42 | VA and PA in very difficult spaces |
J | 0.36–0.40 | 0.38 | Not suitable for PA systems |
U | < 0.36 | — | Not suitable for PA systems |
Three things follow. The familiar “STI ≥ 0.5” requirement of voice-alarm work is band G, whose comment in Table G.1 is literally “target value for VA systems”; Table G.1’s own NOTE 1 adds that its values are minimum targets. The scale is deliberately coarse — one band per 0.04 STI — because that spacing is “based on the typical uncertainty of direct STI measurements”, so a one-band difference is the smallest worth arguing about and quoting three decimals of STI is false precision. And 0.04 is finer than the ≈ 0.03 run-to-run scatter of a single STIPA measurement noted below, so a letter can move between repeats while the STI has not changed in substance. Annexes F and G are informative, and Edition 5’s Scope says outright that the document does not provide criteria for certifying a transmission channel — so write a project requirement as a numeric STI, and use the letter to report it.
From positions to a rating for the space
Section titled “From positions to a rating for the space”The STI is a property of one source-to-listener path, so verifying a hall or a voice-alarm installation is a set of measurements, not one. Put the microphone at ear height for the intended posture (about 1.2 m seated, 1.6 m standing), spread the positions over the served area including the acoustically worst corners rather than the convenient ones, and keep at least one position in each loudspeaker coverage zone of a distributed system: clause 7.6.4 asks for “a representative number of locations”.
The reduction rule is the part that is easy to get wrong. Clause 7.6.4 says
that taking a simple mean of the results can be misleading, and that a
better single figure, one that accounts for the spatial variation, is the
mean minus one standard deviation — sometimes called the rating of the
space, and the value a given location has about an 84 % probability of
reaching if the results are Gaussian. Better still is to plot the whole
statistical distribution. The practical consequence for a fiche: when a
requirement is set, say whether the boxed number is one position, the worst
position or the mean minus one standard deviation, because those are three
different verdicts against the same limit — and the 0.04-wide Annex F bands are
the natural resolution at which to summarise the spread.
Direct or indirect: choosing between them
Section titled “Direct or indirect: choosing between them”Each route has failure modes the standard is explicit about:
- Non-linear or time-variant channels. The indirect method assumes a linear, time-invariant channel: an impulse response cannot represent clipping, compressors, automatic gain control or a vocoder. For a sound system with non-linear processing in the chain, measure directly: the STIPA signal at least travels through the real chain, and the FULL STI signal is the reliable choice where the distortion is severe (IEC 60268-16 clause 6.3 and Table 3).
- Level-dependent effects. The STI is not level-invariant: auditory
masking and the reception threshold act on the absolute band levels at the
listener. Play the test signal at the system’s operating level (the
standard’s Annex J practice sets it 3 dB above the of continuous
speech at the position) and pass
level=andambient=so the analysis includes them; an impulse response measured loud and rescaled afterwards misses these effects entirely. Section 3 moves a measurement made at one level and noise condition to another. - Impulsive and fluctuating background noise. A dropped tool or babble
during a direct measurement corrupts the measured modulation depths
(clause 7.13). The standard’s remedy is the indirect route: average the
impulse response with MLS or sweeps for a noise-free MTF, then add the noise
degradation back via
snr=orlevel=/ambient=. A quick sanity check is to run the analyzer with the source off; the residual STI should stay below 0.20. - Statistical spread. The STIPA signal is pseudo-random noise, so repeated
direct measurements scatter by up to about 0.03 STI even in steady
conditions (and more in fluctuating noise); repeat and compare rather than
trusting a single run, and respect the minimum duration flagged by the
UserWarningabove.
The level dependence is the one of those four that is easy to dismiss, so it is worth seeing how large it is:
Same room, same impulse response, one number that changes by more than a third of the scale. Below about 55 dB the speech is barely clear of the room’s own noise and the reception threshold of Table A.3 bites; above about 80 dB the auditory masking of Table A.2 lets the loud low bands mask the high ones. In between there is a broad plateau, which is why measuring at the operating level is not fussiness — an impulse response measured loud and rescaled afterwards sits on the flat dashed line and is only valid for a level nobody recorded.
Show the code for this figure
# `ir` and `fs` are the reverberant hall of this section.ambient = np.array([45.0, 40.0, 35.0, 30.0, 28.0, 25.0, 22.0]) # dB SPL# The Ed.5 male speech spectrum of clause A.6.1, relative to the 500 Hz band.shape = np.array([-2.5, 0.5, 0.0, -6.0, -12.0, -18.0, -24.0])shape_total = 10 * np.log10(np.sum(10 ** (shape / 10)))
totals = np.arange(40.0, 100.5, 2.5)curve = [speech.sti_from_impulse_response( ir, fs, level=shape - shape_total + t, ambient=ambient).sti for t in totals]print(round(min(curve), 3), round(max(curve), 3)) # 0.246 0.604print(round(speech.sti_from_impulse_response(ir, fs).sti, 3)) # 0.609
fig, ax = plt.subplots()ax.plot(totals, curve)ax.axhline(speech.sti_from_impulse_response(ir, fs).sti, linestyle="--")ax.set_xlabel("Overall speech level at the listener [dB SPL]")ax.set_ylabel("STI")plt.show()sti_from_impulse_response() / stipa() parameters
Section titled “sti_from_impulse_response() / stipa() parameters”| Parameter | Type | Units | Range / default | Notes |
|---|---|---|---|---|
ir / x | 1D array | any / Pa | non-empty | IR (indirect) or STIPA recording (direct) |
fs | int | Hz | > 0 | |
snr | float or 7-vector, optional | dB | default None | Adds steady-noise degradation |
level | 7-vector, optional | dB SPL | default None | Enables auditory masking + reception threshold (Tables A.2/A.3) |
ambient | 7-vector, optional | dB SPL | needs level | Ambient noise band levels |
reference | 1D array, optional (stipa) | — | default None | Measured source signal instead of the nominal |
Both return STIResult: sti, mti (7 bands), mtf (7×14 or 7×2),
band_levels and ambient_levels (the two spectra the corrections used, and
what section 3 reads to move the result to another condition), rating
(Annex F letter A+…U).
IEC 60268-16 report (.report())
Section titled “IEC 60268-16 report (.report())”STIResult.report(path) renders a one-page PDF fiche laid out like a
voice-alarm / public-address intelligibility verification report: a
standard-basis line stating the measurement method (the full STI indirect
method from an impulse response, or the direct STIPA method on a recorded
signal), an optional metadata header block, a per-octave-band modulation
transfer index table beside the per-band MTI bars (the result’s own .plot()),
the boxed STI = X single number with the Annex F qualification band, an
optional verdict row and a footer with the fixed disclaimer. It uses the same
ReportMetadata container and rendering engine as the
ISO 717 insulation fiche;
a supplied requirement is read as the minimum required STI (a higher STI
passes). Rendering needs reportlab and, for the figure the fiche embeds,
matplotlib (pip install "phonometry[report,plot]"); only engine="reportlab"
is supported. Pass language="es" for a Spanish fiche.
from phonometry import ReportMetadata, speech
res = speech.sti_from_impulse_response(ir, fs)res.report( "sti_fiche.pdf", metadata=ReportMetadata( specimen="Concourse voice-alarm loudspeaker line", measurement_standard="IEC 60268-16", laboratory="Phonometry Reference Laboratory", requirement=0.5, # minimum required STI (a higher STI passes) ),)The example fiche is regenerated with make reports and kept rendered in the
repository; click the preview to open the PDF.

One-page speech-transmission-index fiche: a metadata header, an octave-band modulation transfer index table, the per-band MTI bars, the boxed STI = 0.64 single-number result with the Annex F qualification band and a PASS verdict against a 0.5 minimum.
3. Occupancy noise and a different speech level
Section titled “3. Occupancy noise and a different speech level”A hall is measured out of hours, empty, with the test signal at whatever level the amplifier happened to be set to. The rating it has to meet is for the hall in use: an audience in the seats, and the announcement at its operational level. Annex M of IEC 60268-16 moves the measurement to that condition arithmetically, in four steps and without a second visit:
- Acquire. The modulation transfer matrix as measured, with the noise, masking and threshold of the measurement still in it, together with the speech and background-noise octave-band levels present while it was made.
- Remove. Divide out the correction those levels produce, stripping the background noise, the auditory masking of Table A.2 and the reception threshold of Table A.3 back out and leaving the transmission channel alone.
- Reapply. Multiply by the correction the operational levels produce, putting the occupancy noise, masking and threshold of the simulated condition in.
- Process. Run the resulting matrix through the A.5.4 to A.5.6 chain into the index.
Taking the old condition out before putting the new one in is not ceremony, and it is not an approximation either. The measured matrix is a product of two things, and only one of them survives the move: the transmission channel, the room’s decay and the system’s response, is what the empty measurement and the occupied prediction have in common, while the noise, the masking and the threshold belong to the listening condition and go with it. Those three cannot be updated from how far the levels moved, either, because none of them is a function of the change. Auditory masking is a piecewise function of the absolute combined level of the band below, changing slope at 63 dB and again at 67 dB and 100 dB, and the reception threshold of Table A.3 is an absolute intensity that does not move at all. Both have to be re-derived at the new levels, and dividing the old correction out is what leaves something to derive them for.
The shortcut the two steps exist to forbid is the tempting one: running the noise correction over the measured matrix again with the occupancy levels counts the noise twice, because the measurement’s own noise is already in there. Note also where the truncation goes. The channel step 2 recovers can exceed 1.0 in a band whose correction was strong, and it is not clamped between the steps: the truncation of A.5.3 NOTE 1 is for the matrix about to be processed, which is the one step 3 produces. The annex’s own example never tests that, its recovered matrix peaking at 0.997, but a measurement whose correction bites harder would.
For the library the two steps are also one piece of code, which is the other reason to keep them apart. Step 2 divides by the same clause A.5.3 factor a forward measurement multiplies by and step 3 multiplies by it, so the masking of Table A.2 and the threshold of Table A.3 have a single implementation and the adjustment cannot drift away from the measurement it adjusts.
import numpy as np
from phonometry import speech
# The reverberant hall of this section, measured empty: a 0,9 s decay at# 48 kHz stands in for the measured impulse response.fs = 48000rng = np.random.default_rng(0)ir = rng.standard_normal(fs) * np.exp(-6.9078 * np.arange(fs) / fs / 0.9)
# The Ed.5 male speech spectrum of clause A.6.1 at 68 dB overall, and the# empty hall's own ventilation noise, both in dB SPL per octave band.band_shape = np.array([-2.5, 0.5, 0.0, -6.0, -12.0, -18.0, -24.0])speech_empty = band_shape - 10 * np.log10(np.sum(10 ** (band_shape / 10))) + 68.0noise_empty = np.array([42.0, 36.0, 31.0, 28.0, 25.0, 23.0, 21.0])
empty = speech.sti_from_impulse_response( ir, fs, level=speech_empty, ambient=noise_empty)print(f"{empty.sti:.2f} ({empty.rating})") # 0.60 (D)
# The audience in, and the announcement left where it was.noise_occupied = np.array([54.0, 50.0, 47.0, 44.0, 40.0, 35.0, 30.0])occupied = empty.adjusted_for_levels( operational_level=speech_empty, operational_ambient=noise_occupied)print(f"{occupied.sti:.2f} ({occupied.rating})") # 0.56 (F)
# The same room with the announcement 6 dB louder.louder = empty.adjusted_for_levels( operational_level=speech_empty + 6.0, operational_ambient=noise_occupied)print(f"{louder.sti:.2f} ({louder.rating})") # 0.59 (E)The occupancy noise costs two rating letters, and six decibels more from the talker buys one of them back. That is the argument for doing this before the handover rather than after the complaint: the empty hall passed.
STIResult.adjusted_for_levels() reads the measurement condition off the
result, which is why it needs only the operational one and why it refuses a
result computed without level=: such a result carries at most the flat
snr= noise factor, none of the level-dependent masking and threshold, and
no spectra to re-derive them from, so dividing the full correction out would
silently lower the answer. Where the matrix
comes from somewhere else, speech.sti_adjusted_for_levels() takes all four
spectra beside it.
Every result the adjustment returns is an ordinary STIResult, so the per-band
reading of section 2 applies to it, and it is worth taking. The single number
says the hall lost two letters; the bands say where it lost them:
Show the code for this figure
import matplotlib.pyplot as pltimport numpy as npfrom phonometry import speech
fs = 48000rng = np.random.default_rng(0)n = np.arange(fs)ir = rng.standard_normal(fs) * np.exp(-6.9078 * n / fs / 0.9)
shape = np.array([-2.5, 0.5, 0.0, -6.0, -12.0, -18.0, -24.0])talker = shape - 10 * np.log10(np.sum(10 ** (shape / 10))) + 68.0empty_noise = np.array([42.0, 36.0, 31.0, 28.0, 25.0, 23.0, 21.0])full_noise = np.array([54.0, 50.0, 47.0, 44.0, 40.0, 35.0, 30.0])
measured = speech.sti_from_impulse_response(ir, fs, level=talker, ambient=empty_noise)occupied = measured.adjusted_for_levels( operational_level=talker, operational_ambient=full_noise)louder = measured.adjusted_for_levels( operational_level=talker + 6.0, operational_ambient=full_noise)
# One line: the adjusted result is an STIResult, so it draws its own bars.occupied.plot()plt.show()
# By hand, the three conditions on one band axis:positions = np.arange(7)fig, ax = plt.subplots()for i, res in enumerate((measured, occupied, louder)): ax.bar(positions + (i - 1) * 0.27, res.mti, width=0.27, label=f"STI {res.sti:.2f} ({res.rating})")ax.set_xticks(positions)ax.set_xticklabels(["125", "250", "500", "1k", "2k", "4k", "8k"])ax.set_xlabel("Frequency [Hz]")ax.set_ylabel("Modulation transfer index MTI")ax.set_ylim(0.0, 0.95)ax.legend()plt.show()The 125 Hz band gives up the most, because that is where the audience is loudest and the talker has least margin: about 7 dB of speech-to-noise there against 16 dB at 500 Hz. The extra 6 dB then gives back most where most was lost, nearly nine tenths of the 125 Hz loss against about half of the 1 kHz one. Neither of those is visible in the index.
What the adjustment cannot tell you
Section titled “What the adjustment cannot tell you”- It moves the listening condition, not the room. The channel step 2 recovers is carried through untouched, so nothing an audience does to the acoustics is in the answer. Bodies and clothing absorb: the occupied reverberation time is shorter than the empty one, and the occupied channel is in truth better than the one measured. An empty-hall measurement is pessimistic about the reverberation and optimistic about the noise, and this corrects only the second. Where the audience changes the absorption materially, the honest route is an impulse response for the occupied room, measured or predicted, with the adjustment applied to that.
- The operational speech level is an input, not a forecast. The annex says what the STI would be if the talker or the amplifier delivered those band levels. Whether they will is a different question: a live talker lifts their voice in noise, a chain with a limiter or automatic gain control does not simply scale, and a system asked for 6 dB more may clip instead of delivering it, at which point it is no longer the linear channel the measured matrix stands for.
- The noise it models is steady. Occupancy noise is babble, which fluctuates and, being speech-shaped, masks in ways an intensity added per octave band does not represent. The standard’s caution about fluctuating noise (clause 7.13) applies to the condition being simulated as much as to the one being measured.
- The measurement’s own spectra are what step 2 removes. A wrong
measured_levelormeasured_ambienttakes out the wrong correction, and step 3 has no way to notice; the adjusted matrix is then wrong in a direction nothing downstream reports. That is whySTIResult.adjusted_for_levels()reads them off the result instead of asking again. - It is still one listener position. Every spectrum in it is the one at the microphone, so an occupancy-adjusted result answers for that seat and no other.
sti_adjusted_for_levels() parameters
Section titled “sti_adjusted_for_levels() parameters”| Parameter | Type | Units | Range / default | Notes |
|---|---|---|---|---|
mtf | (7, n) array | — | ≥ 0, finite | The matrix as measured, noise and masking included; values above 1 are truncated to 1, and above 1.3 warn that the measurement is likely invalid (A.5.3 NOTE 1) |
measured_level | 7-vector | dB SPL | required | Speech band levels during the measurement |
measured_ambient | 7-vector, optional | dB SPL | default None | Background-noise band levels during the measurement |
operational_level | 7-vector | dB SPL | required | Speech band levels of the condition simulated |
operational_ambient | 7-vector, optional | dB SPL | default None | Occupancy-noise band levels of that condition |
What this guide covers
Section titled “What this guide covers”Covered
IEC 60268-16:2020 (Edition 5) for the male speech option, the only one Edition 5 keeps: the modulation transfer function and the m to STI mapping (clauses A.5.2 to A.5.6), the full-STI indirect method from a measured impulse response via
speech.sti_from_impulse_response(), the direct STIPA method of Annex B viaspeech.stipa_signal()andspeech.stipa(), the Ed.5 male test-signal spectrum of clause A.6.1, the level-dependent auditory masking and reception threshold corrections of Tables A.2 and A.3 (level=andambient=), the Annex F rating letters returned asSTIResult.rating, and the Edition 4 Annex M adjustment of a measured result to occupancy noise and another speech level viaspeech.sti_adjusted_for_levels()andSTIResult.adjusted_for_levels().Not covered
The direct full-STI measurement (the 14-modulation-frequency test signal played and recorded through the real chain, per clause 6.3 and Table 3, recommended above when distortion is severe) is not implemented: only the STIPA direct signal (
stipa_signal/stipa) and the indirect full-STI computation from an impulse response are available. The female speech option is not missing from the library: Edition 5 itself removed it (foreword, item d), so there is nothing left to implement. Whatever Edition 5 added to Annex M (foreword, item g: alternative noise and level adjustments) is neither covered nor ruled out, because that text could not be obtained.
References
Section titled “References”- Houtgast, T., & Steeneken, H. J. M. (1985). A review of the MTF concept in room acoustics and its use for estimating speech intelligibility in auditoria. The Journal of the Acoustical Society of America, 77(3), 1069-1077. https://doi.org/10.1121/1.392224The modulation-transfer framework of section 1 and the m ↔ STI mapping the index is built on.
- International Electrotechnical Commission. (2020). Sound system equipment — Part 16: Objective rating of speech intelligibility by speech transmission index (IEC 60268-16:2020 (Edition 5)). The modulation transfer function and the m ↔ STI mapping, the STIPA test signal and direct method, the indirect method from the impulse response, auditory masking and the reception threshold (Tables A.2/A.3), the revised male speech spectrum (clause A.6.1), the Annex F rating letters and the Annex M adjustment for occupancy noise and speech level. Edition 4's normative PDF is the base and every Ed. 5 change is source-attributed, the only numeric delta being the revised male speech spectrum of clause A.6.1.