1 | /* |
2 | Copyright 2018 Google Inc. All Rights Reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS-IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | // Prevent Visual Studio from complaining about std::copy_n. |
18 | #if defined(_WIN32) |
19 | #define _SCL_SECURE_NO_WARNINGS |
20 | #endif |
21 | |
22 | #include "platforms/common/room_effects_utils.h" |
23 | |
24 | #include <algorithm> |
25 | #include <cmath> |
26 | #include <numeric> |
27 | |
28 | namespace vraudio { |
29 | |
30 | namespace { |
31 | |
32 | // Air absorption coefficients at 20 degrees Celsius and 50% relative humidity, |
33 | // according to: |
34 | // http://www.music.mcgill.ca/~gary/courses/papers/Moorer-Reverb-CMJ-1979.pdf |
35 | // These coefficients have been extrapolated to other frequencies by fitting |
36 | // an exponential curve: |
37 | // |
38 | // m = a + be^(-cx) where: |
39 | // |
40 | // a = -0.00259118 |
41 | // b = 0.003173474 |
42 | // c = -0.0002491554 |
43 | // |
44 | const float kAirAbsorptionCoefficients[]{0.0006f, 0.0006f, 0.0007f, |
45 | 0.0008f, 0.0010f, 0.0015f, |
46 | 0.0026f, 0.0060f, 0.0207f}; |
47 | |
48 | // Room materials with the corresponding absorption coefficients. |
49 | const RoomMaterial kRoomMaterials[static_cast<size_t>( |
50 | MaterialName::kNumMaterialNames)] = { |
51 | {.name: MaterialName::kTransparent, |
52 | // 31.25 62.5 125 250 500 1000 2000 4000 8000 Hz. |
53 | .absorption_coefficients: {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}}, |
54 | {.name: MaterialName::kAcousticCeilingTiles, |
55 | .absorption_coefficients: {0.672f, 0.675f, 0.700f, 0.660f, 0.720f, 0.920f, 0.880f, 0.750f, 1.000f}}, |
56 | {.name: MaterialName::kBrickBare, |
57 | .absorption_coefficients: {0.030f, 0.030f, 0.030f, 0.030f, 0.030f, 0.040f, 0.050f, 0.070f, 0.140f}}, |
58 | |
59 | {.name: MaterialName::kBrickPainted, |
60 | .absorption_coefficients: {0.006f, 0.007f, 0.010f, 0.010f, 0.020f, 0.020f, 0.020f, 0.030f, 0.060f}}, |
61 | |
62 | {.name: MaterialName::kConcreteBlockCoarse, |
63 | .absorption_coefficients: {0.360f, 0.360f, 0.360f, 0.440f, 0.310f, 0.290f, 0.390f, 0.250f, 0.500f}}, |
64 | |
65 | {.name: MaterialName::kConcreteBlockPainted, |
66 | .absorption_coefficients: {0.092f, 0.090f, 0.100f, 0.050f, 0.060f, 0.070f, 0.090f, 0.080f, 0.160f}}, |
67 | |
68 | {.name: MaterialName::kCurtainHeavy, |
69 | .absorption_coefficients: {0.073f, 0.106f, 0.140f, 0.350f, 0.550f, 0.720f, 0.700f, 0.650f, 1.000f}}, |
70 | |
71 | {.name: MaterialName::kFiberGlassInsulation, |
72 | .absorption_coefficients: {0.193f, 0.220f, 0.220f, 0.820f, 0.990f, 0.990f, 0.990f, 0.990f, 1.000f}}, |
73 | |
74 | {.name: MaterialName::kGlassThin, |
75 | .absorption_coefficients: {0.180f, 0.169f, 0.180f, 0.060f, 0.040f, 0.030f, 0.020f, 0.020f, 0.040f}}, |
76 | |
77 | {.name: MaterialName::kGlassThick, |
78 | .absorption_coefficients: {0.350f, 0.350f, 0.350f, 0.250f, 0.180f, 0.120f, 0.070f, 0.040f, 0.080f}}, |
79 | |
80 | {.name: MaterialName::kGrass, |
81 | .absorption_coefficients: {0.05f, 0.05f, 0.15f, 0.25f, 0.40f, 0.55f, 0.60f, 0.60f, 0.60f}}, |
82 | |
83 | {.name: MaterialName::kLinoleumOnConcrete, |
84 | .absorption_coefficients: {0.020f, 0.020f, 0.020f, 0.030f, 0.030f, 0.030f, 0.030f, 0.020f, 0.040f}}, |
85 | |
86 | {.name: MaterialName::kMarble, |
87 | .absorption_coefficients: {0.010f, 0.010f, 0.010f, 0.010f, 0.010f, 0.010f, 0.020f, 0.020f, 0.040f}}, |
88 | |
89 | {.name: MaterialName::kMetal, |
90 | .absorption_coefficients: {0.030f, 0.035f, 0.04f, 0.04f, 0.05f, 0.05f, 0.05f, 0.07f, 0.09f}}, |
91 | |
92 | {.name: MaterialName::kParquetOnConcrete, |
93 | .absorption_coefficients: {0.028f, 0.030f, 0.040f, 0.040f, 0.070f, 0.060f, 0.060f, 0.070f, 0.140f}}, |
94 | |
95 | {.name: MaterialName::kPlasterRough, |
96 | .absorption_coefficients: {0.017f, 0.018f, 0.020f, 0.030f, 0.040f, 0.050f, 0.040f, 0.030f, 0.060f}}, |
97 | |
98 | {.name: MaterialName::kPlasterSmooth, |
99 | .absorption_coefficients: {0.011f, 0.012f, 0.013f, 0.015f, 0.020f, 0.030f, 0.040f, 0.050f, 0.100f}}, |
100 | |
101 | {.name: MaterialName::kPlywoodPanel, |
102 | .absorption_coefficients: {0.40f, 0.34f, 0.28f, 0.22f, 0.17f, 0.09f, 0.10f, 0.11f, 0.22f}}, |
103 | |
104 | {.name: MaterialName::kPolishedConcreteOrTile, |
105 | .absorption_coefficients: {0.008f, 0.008f, 0.010f, 0.010f, 0.015f, 0.020f, 0.020f, 0.020f, 0.040f}}, |
106 | |
107 | {.name: MaterialName::kSheetrock, |
108 | .absorption_coefficients: {0.290f, 0.279f, 0.290f, 0.100f, 0.050f, 0.040f, 0.070f, 0.090f, 0.180f}}, |
109 | |
110 | {.name: MaterialName::kWaterOrIceSurface, |
111 | .absorption_coefficients: {0.006f, 0.006f, 0.008f, 0.008f, 0.013f, 0.015f, 0.020f, 0.025f, 0.050f}}, |
112 | |
113 | {.name: MaterialName::kWoodCeiling, |
114 | .absorption_coefficients: {0.150f, 0.147f, 0.150f, 0.110f, 0.100f, 0.070f, 0.060f, 0.070f, 0.140f}}, |
115 | |
116 | {.name: MaterialName::kWoodPanel, |
117 | .absorption_coefficients: {0.280f, 0.280f, 0.280f, 0.220f, 0.170f, 0.090f, 0.100f, 0.110f, 0.220f}}, |
118 | |
119 | {.name: MaterialName::kUniform, |
120 | .absorption_coefficients: {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f}}}; |
121 | |
122 | // Frequency threshold for low pass filtering. This is the -3dB frequency. |
123 | const float kCutoffFrequency = 800.0f; |
124 | |
125 | // Number of averaging bands for computing the average absorption coefficient. |
126 | const int kNumAveragingBands = 3; |
127 | |
128 | // Average absorption coefficients are computed based on 3 octave bands (500Hz, |
129 | // 1kHz, 2kHz) from the user specified materials. The 500Hz band has index 4. |
130 | const int kStartingBand = 4; |
131 | |
132 | // Default scaling factor of the reverberation tail. This value is |
133 | // multiplied by the user-defined factor in order to allow for the change of |
134 | // gain in the reverb tail from the UI. Updates to the gain value are made to |
135 | // match the previous reverb implementation's loudness. |
136 | const float kDefaultReverbGain = 0.045f; |
137 | |
138 | // Constant used in Eyring's equation to compute RT60. |
139 | const float kEyringConstant = 0.161f; |
140 | |
141 | // Computes RT60 time in the given frequency band according to Eyring. |
142 | inline float ComputeRt60Eyring(float total_area, float mean_absorption, |
143 | size_t band, float volume) { |
144 | return kEyringConstant * volume / |
145 | (-total_area * std::log(x: 1.0f - mean_absorption) + |
146 | 4.0f * kAirAbsorptionCoefficients[band] * volume); |
147 | } |
148 | |
149 | inline std::vector<float> ComputeShoeBoxSurfaceAreas( |
150 | const RoomProperties& room_properties) { |
151 | const float room_width = room_properties.dimensions[0]; |
152 | const float room_height = room_properties.dimensions[1]; |
153 | const float room_depth = room_properties.dimensions[2]; |
154 | const float left_right_area = room_height * room_depth; |
155 | const float top_bottom_area = room_width * room_depth; |
156 | const float front_back_area = room_width * room_height; |
157 | return std::vector<float>{left_right_area, left_right_area, top_bottom_area, |
158 | top_bottom_area, front_back_area, front_back_area}; |
159 | } |
160 | |
161 | // Generates average reflection coefficients for each room model surface. |
162 | // |
163 | // @param room_properties Struct containing properties of the shoe-box room |
164 | // model. |
165 | // @param coefficients Reflection coefficients for each surface. |
166 | void GenerateReflectionCoefficients(const RoomProperties& room_properties, |
167 | float* coefficients) { |
168 | DCHECK(coefficients); |
169 | // Loop through all the surfaces and compute the average absorption |
170 | // coefficient for 3 bands (500Hz, 1kHz and 2kHz). |
171 | for (size_t surface = 0; surface < kNumRoomSurfaces; ++surface) { |
172 | const size_t material_index = |
173 | static_cast<size_t>(room_properties.material_names[surface]); |
174 | // Absorption coefficients in all bands for the current surface. |
175 | const auto& absorption_coefficients = |
176 | kRoomMaterials[material_index].absorption_coefficients; |
177 | // Compute average absorption coefficients for each surface. |
178 | float average_absorption_coefficient = |
179 | std::accumulate(first: std::begin(arr: absorption_coefficients) + kStartingBand, |
180 | last: std::begin(arr: absorption_coefficients) + kStartingBand + |
181 | kNumAveragingBands, |
182 | init: 0.0f) / |
183 | static_cast<float>(kNumAveragingBands); |
184 | // Compute a reflection coefficient for each surface. |
185 | coefficients[surface] = |
186 | std::min(a: 1.0f, b: std::sqrt(x: 1.0f - average_absorption_coefficient)); |
187 | } |
188 | } |
189 | |
190 | // Uses the Eyring's equation to estimate RT60 values (reverb time in seconds) |
191 | // in |kNumReverbOctaveBands| octave bands, including the correction for air |
192 | // absorption. The equation is applied as defined in: |
193 | // https://arauacustica.com/files/publicaciones_relacionados/pdf_esp_26.pdf |
194 | // |
195 | |
196 | void GenerateRt60Values(const RoomProperties& room_properties, |
197 | float* rt60_values) { |
198 | DCHECK(rt60_values); |
199 | // Compute the shoe-box room volume. |
200 | const float room_volume = room_properties.dimensions[0] * |
201 | room_properties.dimensions[1] * |
202 | room_properties.dimensions[2]; |
203 | if (room_volume < std::numeric_limits<float>::epsilon()) { |
204 | // RT60 values will be all zeros, if the room volume is zero. |
205 | return; |
206 | } |
207 | |
208 | // Compute surface areas of the shoe-box room. |
209 | const std::vector<float> all_surface_areas = |
210 | ComputeShoeBoxSurfaceAreas(room_properties); |
211 | const float total_area = |
212 | std::accumulate(first: all_surface_areas.begin(), last: all_surface_areas.end(), init: 0.0f); |
213 | DCHECK_GT(total_area, 0.0f); |
214 | // Loop through each band and compute the RT60 values. |
215 | for (size_t band = 0; band < kNumReverbOctaveBands; ++band) { |
216 | // Initialize the effective absorbing area. |
217 | float absorbing_area = 0.0f; |
218 | for (size_t surface = 0; surface < kNumRoomSurfaces; ++surface) { |
219 | const size_t material_index = |
220 | static_cast<size_t>(room_properties.material_names[surface]); |
221 | // Compute the effective absorbing area based on the absorption |
222 | // coefficients for the current band and all the surfaces. |
223 | absorbing_area += |
224 | kRoomMaterials[material_index].absorption_coefficients[band] * |
225 | all_surface_areas[surface]; |
226 | } |
227 | DCHECK_GT(absorbing_area, 0.0f); |
228 | const float mean_absorption = std::min(a: absorbing_area / total_area, b: 1.0f); |
229 | |
230 | // Compute RT60 time in this band according to Eyring. |
231 | rt60_values[band] = |
232 | ComputeRt60Eyring(total_area, mean_absorption, band, volume: room_volume); |
233 | } |
234 | } |
235 | |
236 | // Modifies the RT60 values by the given |brightness_modifier| and |
237 | // |time_scaler|. |
238 | void ModifyRT60Values(float brightness_modifier, float time_scaler, |
239 | float* rt60_values) { |
240 | DCHECK(rt60_values); |
241 | for (size_t band = 0; band < kNumReverbOctaveBands; ++band) { |
242 | // Linearly scale calculated reverb times according to the user specified |
243 | // |brightness_modifier| and |time_scaler| values. |
244 | rt60_values[band] *= |
245 | (1.0f + brightness_modifier * static_cast<float>(band + 1) / |
246 | static_cast<float>(kNumReverbOctaveBands)) * |
247 | time_scaler; |
248 | } |
249 | } |
250 | |
251 | } // namespace |
252 | |
253 | ReflectionProperties ComputeReflectionProperties( |
254 | const RoomProperties& room_properties) { |
255 | ReflectionProperties reflection_properties; |
256 | std::copy(first: std::begin(arr: room_properties.position), |
257 | last: std::end(arr: room_properties.position), |
258 | result: std::begin(arr&: reflection_properties.room_position)); |
259 | std::copy(first: std::begin(arr: room_properties.rotation), |
260 | last: std::end(arr: room_properties.rotation), |
261 | result: std::begin(arr&: reflection_properties.room_rotation)); |
262 | std::copy(first: std::begin(arr: room_properties.dimensions), |
263 | last: std::end(arr: room_properties.dimensions), |
264 | result: std::begin(arr&: reflection_properties.room_dimensions)); |
265 | reflection_properties.cutoff_frequency = kCutoffFrequency; |
266 | GenerateReflectionCoefficients(room_properties, |
267 | coefficients: reflection_properties.coefficients); |
268 | reflection_properties.gain = room_properties.reflection_scalar; |
269 | return reflection_properties; |
270 | } |
271 | |
272 | ReverbProperties ComputeReverbProperties( |
273 | const RoomProperties& room_properties) { |
274 | ReverbProperties reverb_properties; |
275 | GenerateRt60Values(room_properties, rt60_values: reverb_properties.rt60_values); |
276 | ModifyRT60Values(brightness_modifier: room_properties.reverb_brightness, |
277 | time_scaler: room_properties.reverb_time, rt60_values: reverb_properties.rt60_values); |
278 | reverb_properties.gain = kDefaultReverbGain * room_properties.reverb_gain; |
279 | return reverb_properties; |
280 | } |
281 | |
282 | ReverbProperties ComputeReverbPropertiesFromRT60s(const float* rt60_values, |
283 | float brightness_modifier, |
284 | float time_scalar, |
285 | float gain_multiplier) { |
286 | DCHECK(rt60_values); |
287 | |
288 | ReverbProperties reverb_properties; |
289 | std::copy_n(first: rt60_values, n: kNumReverbOctaveBands, |
290 | result: reverb_properties.rt60_values); |
291 | ModifyRT60Values(brightness_modifier, time_scaler: time_scalar, |
292 | rt60_values: reverb_properties.rt60_values); |
293 | reverb_properties.gain = kDefaultReverbGain * gain_multiplier; |
294 | return reverb_properties; |
295 | } |
296 | |
297 | float ComputeRoomEffectsGain(const WorldPosition& source_position, |
298 | const WorldPosition& room_position, |
299 | const WorldRotation& room_rotation, |
300 | const WorldPosition& room_dimensions) { |
301 | const float room_volume = |
302 | room_dimensions[0] * room_dimensions[1] * room_dimensions[2]; |
303 | if (room_volume < std::numeric_limits<float>::epsilon()) { |
304 | // No room effects should be present when the room volume is zero. |
305 | return 0.0f; |
306 | } |
307 | |
308 | // Compute the relative source position with respect to the room. |
309 | WorldPosition relative_source_position; |
310 | GetRelativeDirection(from_position: room_position, from_rotation: room_rotation, to_position: source_position, |
311 | relative_direction: &relative_source_position); |
312 | WorldPosition closest_position_in_room; |
313 | GetClosestPositionInAabb(relative_position: relative_source_position, aabb_dimensions: room_dimensions, |
314 | closest_position: &closest_position_in_room); |
315 | // Shift the attenuation curve by 1.0f to avoid zero division. |
316 | const float distance_to_room = |
317 | 1.0f + (relative_source_position - closest_position_in_room).norm(); |
318 | return 1.0f / (distance_to_room * distance_to_room); |
319 | } |
320 | |
321 | RoomMaterial GetRoomMaterial(size_t material_index) { |
322 | DCHECK_LT(material_index, |
323 | static_cast<size_t>(MaterialName::kNumMaterialNames)); |
324 | return kRoomMaterials[material_index]; |
325 | } |
326 | |
327 | } // namespace vraudio |
328 | |