| 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 | |