summaryrefslogtreecommitdiffstats
path: root/src/3rdparty/resonance-audio/platforms/common/room_effects_utils.cc
blob: 3c74f26dc39f728314937c5ec1a9a5f5872c9bcb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/*
Copyright 2018 Google Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS-IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Prevent Visual Studio from complaining about std::copy_n.
#if defined(_WIN32)
#define _SCL_SECURE_NO_WARNINGS
#endif

#include "platforms/common/room_effects_utils.h"

#include <algorithm>
#include <cmath>
#include <numeric>

namespace vraudio {

namespace {

// Air absorption coefficients at 20 degrees Celsius and 50% relative humidity,
// according to:
// http://www.music.mcgill.ca/~gary/courses/papers/Moorer-Reverb-CMJ-1979.pdf
// These coefficients have been extrapolated to other frequencies by fitting
// an exponential curve:
//
// m = a + be^(-cx) where:
//
// a = -0.00259118
// b = 0.003173474
// c = -0.0002491554
//
const float kAirAbsorptionCoefficients[]{0.0006f, 0.0006f, 0.0007f,
                                         0.0008f, 0.0010f, 0.0015f,
                                         0.0026f, 0.0060f, 0.0207f};

// Room materials with the corresponding absorption coefficients.
const RoomMaterial kRoomMaterials[static_cast<size_t>(
    MaterialName::kNumMaterialNames)] = {
    {MaterialName::kTransparent,
     // 31.25 62.5  125   250   500   1000  2000  4000  8000 Hz.
     {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}},
    {MaterialName::kAcousticCeilingTiles,
     {0.672f, 0.675f, 0.700f, 0.660f, 0.720f, 0.920f, 0.880f, 0.750f, 1.000f}},
    {MaterialName::kBrickBare,
     {0.030f, 0.030f, 0.030f, 0.030f, 0.030f, 0.040f, 0.050f, 0.070f, 0.140f}},

    {MaterialName::kBrickPainted,
     {0.006f, 0.007f, 0.010f, 0.010f, 0.020f, 0.020f, 0.020f, 0.030f, 0.060f}},

    {MaterialName::kConcreteBlockCoarse,
     {0.360f, 0.360f, 0.360f, 0.440f, 0.310f, 0.290f, 0.390f, 0.250f, 0.500f}},

    {MaterialName::kConcreteBlockPainted,
     {0.092f, 0.090f, 0.100f, 0.050f, 0.060f, 0.070f, 0.090f, 0.080f, 0.160f}},

    {MaterialName::kCurtainHeavy,
     {0.073f, 0.106f, 0.140f, 0.350f, 0.550f, 0.720f, 0.700f, 0.650f, 1.000f}},

    {MaterialName::kFiberGlassInsulation,
     {0.193f, 0.220f, 0.220f, 0.820f, 0.990f, 0.990f, 0.990f, 0.990f, 1.000f}},

    {MaterialName::kGlassThin,
     {0.180f, 0.169f, 0.180f, 0.060f, 0.040f, 0.030f, 0.020f, 0.020f, 0.040f}},

    {MaterialName::kGlassThick,
     {0.350f, 0.350f, 0.350f, 0.250f, 0.180f, 0.120f, 0.070f, 0.040f, 0.080f}},

    {MaterialName::kGrass,
     {0.05f, 0.05f, 0.15f, 0.25f, 0.40f, 0.55f, 0.60f, 0.60f, 0.60f}},

    {MaterialName::kLinoleumOnConcrete,
     {0.020f, 0.020f, 0.020f, 0.030f, 0.030f, 0.030f, 0.030f, 0.020f, 0.040f}},

    {MaterialName::kMarble,
     {0.010f, 0.010f, 0.010f, 0.010f, 0.010f, 0.010f, 0.020f, 0.020f, 0.040f}},

    {MaterialName::kMetal,
     {0.030f, 0.035f, 0.04f, 0.04f, 0.05f, 0.05f, 0.05f, 0.07f, 0.09f}},

    {MaterialName::kParquetOnConcrete,
     {0.028f, 0.030f, 0.040f, 0.040f, 0.070f, 0.060f, 0.060f, 0.070f, 0.140f}},

    {MaterialName::kPlasterRough,
     {0.017f, 0.018f, 0.020f, 0.030f, 0.040f, 0.050f, 0.040f, 0.030f, 0.060f}},

    {MaterialName::kPlasterSmooth,
     {0.011f, 0.012f, 0.013f, 0.015f, 0.020f, 0.030f, 0.040f, 0.050f, 0.100f}},

    {MaterialName::kPlywoodPanel,
     {0.40f, 0.34f, 0.28f, 0.22f, 0.17f, 0.09f, 0.10f, 0.11f, 0.22f}},

    {MaterialName::kPolishedConcreteOrTile,
     {0.008f, 0.008f, 0.010f, 0.010f, 0.015f, 0.020f, 0.020f, 0.020f, 0.040f}},

    {MaterialName::kSheetrock,
     {0.290f, 0.279f, 0.290f, 0.100f, 0.050f, 0.040f, 0.070f, 0.090f, 0.180f}},

    {MaterialName::kWaterOrIceSurface,
     {0.006f, 0.006f, 0.008f, 0.008f, 0.013f, 0.015f, 0.020f, 0.025f, 0.050f}},

    {MaterialName::kWoodCeiling,
     {0.150f, 0.147f, 0.150f, 0.110f, 0.100f, 0.070f, 0.060f, 0.070f, 0.140f}},

    {MaterialName::kWoodPanel,
     {0.280f, 0.280f, 0.280f, 0.220f, 0.170f, 0.090f, 0.100f, 0.110f, 0.220f}},

    {MaterialName::kUniform,
     {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f}}};

// Frequency threshold for low pass filtering. This is the -3dB frequency.
const float kCutoffFrequency = 800.0f;

// Number of averaging bands for computing the average absorption coefficient.
const int kNumAveragingBands = 3;

// Average absorption coefficients are computed based on 3 octave bands (500Hz,
// 1kHz, 2kHz) from the user specified materials. The 500Hz band has index 4.
const int kStartingBand = 4;

// Default scaling factor of the reverberation tail. This value is
// multiplied by the user-defined factor in order to allow for the change of
// gain in the reverb tail from the UI. Updates to the gain value are made to
// match the previous reverb implementation's loudness.
const float kDefaultReverbGain = 0.045f;

// Constant used in Eyring's equation to compute RT60.
const float kEyringConstant = 0.161f;

// Computes RT60 time in the given frequency band according to Eyring.
inline float ComputeRt60Eyring(float total_area, float mean_absorption,
                               size_t band, float volume) {
  return kEyringConstant * volume /
         (-total_area * std::log(1.0f - mean_absorption) +
          4.0f * kAirAbsorptionCoefficients[band] * volume);
}

inline std::vector<float> ComputeShoeBoxSurfaceAreas(
    const RoomProperties& room_properties) {
  const float room_width = room_properties.dimensions[0];
  const float room_height = room_properties.dimensions[1];
  const float room_depth = room_properties.dimensions[2];
  const float left_right_area = room_height * room_depth;
  const float top_bottom_area = room_width * room_depth;
  const float front_back_area = room_width * room_height;
  return std::vector<float>{left_right_area, left_right_area, top_bottom_area,
                            top_bottom_area, front_back_area, front_back_area};
}

// Generates average reflection coefficients for each room model surface.
//
// @param room_properties Struct containing properties of the shoe-box room
//     model.
// @param coefficients Reflection coefficients for each surface.
void GenerateReflectionCoefficients(const RoomProperties& room_properties,
                                    float* coefficients) {
  DCHECK(coefficients);
  // Loop through all the surfaces and compute the average absorption
  // coefficient for 3 bands (500Hz, 1kHz and 2kHz).
  for (size_t surface = 0; surface < kNumRoomSurfaces; ++surface) {
    const size_t material_index =
        static_cast<size_t>(room_properties.material_names[surface]);
    // Absorption coefficients in all bands for the current surface.
    const auto& absorption_coefficients =
        kRoomMaterials[material_index].absorption_coefficients;
    // Compute average absorption coefficients for each surface.
    float average_absorption_coefficient =
        std::accumulate(std::begin(absorption_coefficients) + kStartingBand,
                        std::begin(absorption_coefficients) + kStartingBand +
                            kNumAveragingBands,
                        0.0f) /
        static_cast<float>(kNumAveragingBands);
    // Compute a reflection coefficient for each surface.
    coefficients[surface] =
        std::min(1.0f, std::sqrt(1.0f - average_absorption_coefficient));
  }
}

// Uses the Eyring's equation to estimate RT60 values (reverb time in seconds)
// in |kNumReverbOctaveBands| octave bands, including the correction for air
// absorption. The equation is applied as defined in:
// https://arauacustica.com/files/publicaciones_relacionados/pdf_esp_26.pdf
//

void GenerateRt60Values(const RoomProperties& room_properties,
                        float* rt60_values) {
  DCHECK(rt60_values);
  // Compute the shoe-box room volume.
  const float room_volume = room_properties.dimensions[0] *
                            room_properties.dimensions[1] *
                            room_properties.dimensions[2];
  if (room_volume < std::numeric_limits<float>::epsilon()) {
    // RT60 values will be all zeros, if the room volume is zero.
    return;
  }

  // Compute surface areas of the shoe-box room.
  const std::vector<float> all_surface_areas =
      ComputeShoeBoxSurfaceAreas(room_properties);
  const float total_area =
      std::accumulate(all_surface_areas.begin(), all_surface_areas.end(), 0.0f);
  DCHECK_GT(total_area, 0.0f);
  // Loop through each band and compute the RT60 values.
  for (size_t band = 0; band < kNumReverbOctaveBands; ++band) {
    // Initialize the effective absorbing area.
    float absorbing_area = 0.0f;
    for (size_t surface = 0; surface < kNumRoomSurfaces; ++surface) {
      const size_t material_index =
          static_cast<size_t>(room_properties.material_names[surface]);
      // Compute the effective absorbing area based on the absorption
      // coefficients for the current band and all the surfaces.
      absorbing_area +=
          kRoomMaterials[material_index].absorption_coefficients[band] *
          all_surface_areas[surface];
    }
    DCHECK_GT(absorbing_area, 0.0f);
    const float mean_absorption = std::min(absorbing_area / total_area, 1.0f);

    // Compute RT60 time in this band according to Eyring.
    rt60_values[band] =
        ComputeRt60Eyring(total_area, mean_absorption, band, room_volume);
  }
}

// Modifies the RT60 values by the given |brightness_modifier| and
// |time_scaler|.
void ModifyRT60Values(float brightness_modifier, float time_scaler,
                      float* rt60_values) {
  DCHECK(rt60_values);
  for (size_t band = 0; band < kNumReverbOctaveBands; ++band) {
    // Linearly scale calculated reverb times according to the user specified
    // |brightness_modifier| and |time_scaler| values.
    rt60_values[band] *=
        (1.0f + brightness_modifier * static_cast<float>(band + 1) /
                    static_cast<float>(kNumReverbOctaveBands)) *
        time_scaler;
  }
}

}  // namespace

ReflectionProperties ComputeReflectionProperties(
    const RoomProperties& room_properties) {
  ReflectionProperties reflection_properties;
  std::copy(std::begin(room_properties.position),
            std::end(room_properties.position),
            std::begin(reflection_properties.room_position));
  std::copy(std::begin(room_properties.rotation),
            std::end(room_properties.rotation),
            std::begin(reflection_properties.room_rotation));
  std::copy(std::begin(room_properties.dimensions),
            std::end(room_properties.dimensions),
            std::begin(reflection_properties.room_dimensions));
  reflection_properties.cutoff_frequency = kCutoffFrequency;
  GenerateReflectionCoefficients(room_properties,
                                 reflection_properties.coefficients);
  reflection_properties.gain = room_properties.reflection_scalar;
  return reflection_properties;
}

ReverbProperties ComputeReverbProperties(
    const RoomProperties& room_properties) {
  ReverbProperties reverb_properties;
  GenerateRt60Values(room_properties, reverb_properties.rt60_values);
  ModifyRT60Values(room_properties.reverb_brightness,
                   room_properties.reverb_time, reverb_properties.rt60_values);
  reverb_properties.gain = kDefaultReverbGain * room_properties.reverb_gain;
  return reverb_properties;
}

ReverbProperties ComputeReverbPropertiesFromRT60s(const float* rt60_values,
                                                  float brightness_modifier,
                                                  float time_scalar,
                                                  float gain_multiplier) {
  DCHECK(rt60_values);

  ReverbProperties reverb_properties;
  std::copy_n(rt60_values, kNumReverbOctaveBands,
              reverb_properties.rt60_values);
  ModifyRT60Values(brightness_modifier, time_scalar,
                   reverb_properties.rt60_values);
  reverb_properties.gain = kDefaultReverbGain * gain_multiplier;
  return reverb_properties;
}

float ComputeRoomEffectsGain(const WorldPosition& source_position,
                             const WorldPosition& room_position,
                             const WorldRotation& room_rotation,
                             const WorldPosition& room_dimensions) {
  const float room_volume =
      room_dimensions[0] * room_dimensions[1] * room_dimensions[2];
  if (room_volume < std::numeric_limits<float>::epsilon()) {
    // No room effects should be present when the room volume is zero.
    return 0.0f;
  }

  // Compute the relative source position with respect to the room.
  WorldPosition relative_source_position;
  GetRelativeDirection(room_position, room_rotation, source_position,
                       &relative_source_position);
  WorldPosition closest_position_in_room;
  GetClosestPositionInAabb(relative_source_position, room_dimensions,
                           &closest_position_in_room);
  // Shift the attenuation curve by 1.0f to avoid zero division.
  const float distance_to_room =
      1.0f + (relative_source_position - closest_position_in_room).norm();
  return 1.0f / (distance_to_room * distance_to_room);
}

RoomMaterial GetRoomMaterial(size_t material_index) {
  DCHECK_LT(material_index,
            static_cast<size_t>(MaterialName::kNumMaterialNames));
  return kRoomMaterials[material_index];
}

}  // namespace vraudio