|
4 | 4 | """ |
5 | 5 | import re |
6 | 6 | import numpy as np |
7 | | -import pyfar.signals as pysi |
8 | | -from . import dsp |
| 7 | +import pyfar as pf |
9 | 8 | import warnings |
10 | 9 |
|
11 | 10 |
|
@@ -110,85 +109,106 @@ def reverberation_time_linear_regression( |
110 | 109 |
|
111 | 110 |
|
112 | 111 |
|
113 | | -import numpy as np |
114 | | -import pyfar as pf |
115 | | -import pyfar.dsp as dsp |
116 | 112 |
|
117 | 113 | def clarity(RIR, early_time_limit=80): |
118 | | - """Calculate the clarity of a signal in a room. |
119 | | - |
120 | | - The clarity parameter is calculated with the early-to-late index at 50 ms or 80 ms and describes how |
121 | | - clearly someone can hear sound and music in a room |
| 114 | + """ |
| 115 | + Calculate the clarity of a room impulse response. |
| 116 | +
|
| 117 | + The clarity parameter (C50 or C80) is defined as the ratio of early-to-late |
| 118 | + arriving energy in an impulse response and describes how clearly speech or |
| 119 | + music can be perceived in a room. The early-to-late boundary is typically |
| 120 | + set at 50 ms (C50) or 80 ms (C80). |
122 | 121 |
|
123 | 122 | Parameters |
124 | 123 | ---------- |
125 | 124 | RIR : pyfar.Signal |
126 | | - Room impulse response (or energy decay curve) |
127 | | - early_time_limit : float [s] |
128 | | - Early time limit to calculate the clarity as a scalar in seconds |
129 | | - Typically 0.05 (C50) or 0.08 (C80). |
| 125 | + Room impulse response (time-domain signal). |
| 126 | + early_time_limit : float, optional |
| 127 | + Early time limit in milliseconds. Defaults to 80 (C80). Typical values |
| 128 | + are 50 ms (C50) or 80 ms (C80). |
| 129 | + frequencies : None or ndarray, optional |
| 130 | + Placeholder for octave/third-octave band center frequencies if the |
| 131 | + result should be returned as a frequency-domain representation. The |
| 132 | + filtering must be applied by the user prior to calling this function. |
130 | 133 |
|
131 | 134 | Returns |
132 | 135 | ------- |
133 | | - clarity : ndarray [dB] |
134 | | - Clarity index (early-to-late energy ratio) in decibel, |
135 | | - shaped according to the channel structure of RIR. |
| 136 | + clarity : ndarray of float |
| 137 | + Clarity index (early-to-late energy ratio) in decibels, shaped according |
| 138 | + to the channel structure of ``RIR``. |
| 139 | +
|
| 140 | + References |
| 141 | + ---------- |
| 142 | + ISO 3382-1 : Annex A |
| 143 | +
|
| 144 | + Examples |
| 145 | + -------- |
| 146 | +
|
| 147 | + Estimate the clarity from a real room impulse response and octave-band |
| 148 | + filtering: |
| 149 | +
|
| 150 | + >>> import numpy as np |
| 151 | + >>> import pyfar as pf |
| 152 | + >>> import pyrato as ra |
| 153 | + >>> RIR = pf.signals.files.room_impulse_response(sampling_rate=44100) |
| 154 | + >>> RIR = pf.dsp.filter.fractional_octave_bands(RIR, bands_per_octave=3) |
| 155 | + >>> C80 = ra.parameters.clarity(RIR, early_time_limit=80) |
136 | 156 |
|
137 | | - Reference |
138 | | - --------- |
139 | | - ISO3382-1 : Annex A |
140 | 157 | """ |
| 158 | + |
141 | 159 | if not hasattr(RIR, "cshape") or not hasattr(RIR, "sampling_rate"): |
142 | | - raise AttributeError("clarity() requires a Signal object as input.") |
| 160 | + raise AttributeError("clarity() requires a signal object as input.") |
143 | 161 |
|
144 | 162 | # warnign for unusual early_time_limit |
145 | 163 | if early_time_limit not in (50, 80): |
146 | 164 | warnings.warn( |
147 | | - f"early_time_limit={early_time_limit}s is unusual. " |
148 | | - "Typically 50ms (C50) or 80ms (C80) are used.", |
| 165 | + f"early_time_limit={early_time_limit}ms is unusual. " |
| 166 | + "Typically 50ms (C50) or 80ms (C80) are chosen.", |
149 | 167 | UserWarning |
150 | 168 | ) |
| 169 | + signal_length_ms = (RIR.signal_length) * 1000 |
| 170 | + if early_time_limit > signal_length_ms: |
| 171 | + raise ValueError("early_time_limit cannot be larger than signal length.") |
| 172 | + if early_time_limit <= 0: |
| 173 | + raise ValueError("early_time_limit must be positive.") |
| 174 | + |
| 175 | + if RIR.complex: |
| 176 | + warnings.warn( |
| 177 | + "Complex-valued input detected. Clarity is only defined for real " |
| 178 | + "signals and will be computed using |x(t)|^2.", |
| 179 | + UserWarning, |
| 180 | + ) |
| 181 | + |
| 182 | + # convert milliseconds to seconds for index lookup |
| 183 | + early_time_limit_sec = early_time_limit / 1000 |
151 | 184 |
|
152 | | - # get channel shape & flatten audio object |
153 | 185 | channel_shape = RIR.cshape |
154 | 186 | RIR_flat = RIR.flatten() |
155 | 187 |
|
156 | 188 | clarity_vals = [] |
157 | 189 |
|
158 | | - # iterate over flattended channels |
159 | 190 | for rir in RIR_flat: |
| 191 | + start_index = pf.dsp.find_impulse_response_start(rir)[0] |
| 192 | + early_time_limit_index = int(rir.find_nearest_time(early_time_limit_sec)) |
160 | 193 |
|
161 | | - # start-index |
162 | | - start_index = dsp.find_impulse_response_start(rir)[0] |
163 | | - |
164 | | - # early_time_limit-index |
165 | | - early_time_limit_index = int(rir.find_nearest_time(early_time_limit/1000)) |
| 194 | + # energy from squared amplitude |
| 195 | + energy_decay = np.abs(rir.time) ** 2 |
166 | 196 |
|
167 | | - # calculate edc |
168 | | - if rir.signal_type == "energy": |
169 | | - energy_decay = rir.time |
170 | | - else: |
171 | | - energy_decay = rir.time**2 |
172 | | - |
173 | | - # late- and early energy |
174 | | - energy_decay_early = np.sum(energy_decay[:,start_index:early_time_limit_index]) |
175 | | - energy_decay_late = np.sum(energy_decay[:,early_time_limit_index:]) |
| 197 | + early_energy = np.sum(energy_decay[:, start_index:early_time_limit_index]) |
| 198 | + late_energy = np.sum(energy_decay[:, early_time_limit_index:]) |
176 | 199 |
|
177 | | - # clarity fraction incl. edge case handling |
178 | | - if energy_decay_early == 0 and energy_decay_late == 0: |
| 200 | + if early_energy == 0 and late_energy == 0: |
179 | 201 | val = np.nan |
180 | | - elif energy_decay_early == 0: |
| 202 | + elif early_energy == 0: |
181 | 203 | val = -np.inf |
182 | | - elif energy_decay_late == 0: |
| 204 | + elif late_energy == 0: |
183 | 205 | val = np.inf |
184 | 206 | else: |
185 | | - val = 10 * np.log10(energy_decay_early / energy_decay_late) |
| 207 | + val = 10 * np.log10(early_energy / late_energy) |
186 | 208 |
|
187 | 209 | clarity_vals.append(val) |
188 | 210 |
|
189 | | - # reshape array to channel_shape |
190 | | - clarity_vals = np.array(clarity_vals).reshape(channel_shape) |
191 | | - |
192 | | - return clarity_vals |
| 211 | + clarity = np.array(clarity_vals).reshape(channel_shape) |
| 212 | + return clarity |
193 | 213 |
|
194 | 214 |
|
0 commit comments