summaryrefslogtreecommitdiffstats
path: root/dom/media/webaudio/blink/HRTFElevation.cpp
blob: af4e520384046c13cc702a9ed8fa853aac1de9fb (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
328
329
330
331
332
333
334
335
336
337
338
/*
 * Copyright (C) 2010 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "HRTFElevation.h"

#include <speex/speex_resampler.h>
#include "mozilla/PodOperations.h"
#include "AudioSampleFormat.h"

#include "IRC_Composite_C_R0195-incl.cpp"

using namespace mozilla;

namespace WebCore {

const int elevationSpacing = irc_composite_c_r0195_elevation_interval;
const int firstElevation = irc_composite_c_r0195_first_elevation;
const int numberOfElevations = MOZ_ARRAY_LENGTH(irc_composite_c_r0195);

const unsigned HRTFElevation::NumberOfTotalAzimuths = 360 / 15 * 8;

const int rawSampleRate = irc_composite_c_r0195_sample_rate;

// Number of frames in an individual impulse response.
const size_t ResponseFrameSize = 256;

size_t HRTFElevation::sizeOfIncludingThis(
    mozilla::MallocSizeOf aMallocSizeOf) const {
  size_t amount = aMallocSizeOf(this);

  amount += m_kernelListL.ShallowSizeOfExcludingThis(aMallocSizeOf);
  for (size_t i = 0; i < m_kernelListL.Length(); i++) {
    amount += m_kernelListL[i]->sizeOfIncludingThis(aMallocSizeOf);
  }

  return amount;
}

size_t HRTFElevation::fftSizeForSampleRate(float sampleRate) {
  // The IRCAM HRTF impulse responses were 512 sample-frames @44.1KHz,
  // but these have been truncated to 256 samples.
  // An FFT-size of twice impulse response size is used (for convolution).
  // So for sample rates of 44.1KHz an FFT size of 512 is good.
  // We double the FFT-size only for sample rates at least double this.
  // If the FFT size is too large then the impulse response will be padded
  // with zeros without the fade-out provided by HRTFKernel.
  MOZ_ASSERT(sampleRate > 1.0 && sampleRate < 1048576.0);

  // This is the size if we were to use all raw response samples.
  unsigned resampledLength =
      floorf(ResponseFrameSize * sampleRate / rawSampleRate);
  // Keep things semi-sane, with max FFT size of 1024.
  unsigned size = std::min(resampledLength, 1023U);
  // Ensure a minimum of 2 * WEBAUDIO_BLOCK_SIZE (with the size++ below) for
  // FFTConvolver and set the 8 least significant bits for rounding up to
  // the next power of 2 below.
  size |= 2 * WEBAUDIO_BLOCK_SIZE - 1;
  // Round up to the next power of 2, making the FFT size no more than twice
  // the impulse response length.  This doubles size for values that are
  // already powers of 2.  This works by filling in alls bit to right of the
  // most significant bit.  The most significant bit is no greater than
  // 1 << 9, and the least significant 8 bits were already set above, so
  // there is at most one bit to add.
  size |= (size >> 1);
  size++;
  MOZ_ASSERT((size & (size - 1)) == 0);

  return size;
}

nsReturnRef<HRTFKernel> HRTFElevation::calculateKernelForAzimuthElevation(
    int azimuth, int elevation, SpeexResamplerState* resampler,
    float sampleRate) {
  int elevationIndex = (elevation - firstElevation) / elevationSpacing;
  MOZ_ASSERT(elevationIndex >= 0 && elevationIndex <= numberOfElevations);

  int numberOfAzimuths = irc_composite_c_r0195[elevationIndex].count;
  int azimuthSpacing = 360 / numberOfAzimuths;
  MOZ_ASSERT(numberOfAzimuths * azimuthSpacing == 360);

  int azimuthIndex = azimuth / azimuthSpacing;
  MOZ_ASSERT(azimuthIndex * azimuthSpacing == azimuth);

  const int16_t(&impulse_response_data)[ResponseFrameSize] =
      irc_composite_c_r0195[elevationIndex].azimuths[azimuthIndex];

  // When libspeex_resampler is compiled with FIXED_POINT, samples in
  // speex_resampler_process_float are rounded directly to int16_t, which
  // only works well if the floats are in the range +/-32767.  On such
  // platforms it's better to resample before converting to float anyway.
#ifdef MOZ_SAMPLE_TYPE_S16
#  define RESAMPLER_PROCESS speex_resampler_process_int
  const int16_t* response = impulse_response_data;
  const int16_t* resampledResponse;
#else
#  define RESAMPLER_PROCESS speex_resampler_process_float
  float response[ResponseFrameSize];
  ConvertAudioSamples(impulse_response_data, response, ResponseFrameSize);
  float* resampledResponse;
#endif

  // Note that depending on the fftSize returned by the panner, we may be
  // truncating the impulse response.
  const size_t resampledResponseLength = fftSizeForSampleRate(sampleRate) / 2;

  AutoTArray<AudioDataValue, 2 * ResponseFrameSize> resampled;
  if (sampleRate == rawSampleRate) {
    resampledResponse = response;
    MOZ_ASSERT(resampledResponseLength == ResponseFrameSize);
  } else {
    resampled.SetLength(resampledResponseLength);
    resampledResponse = resampled.Elements();
    speex_resampler_skip_zeros(resampler);

    // Feed the input buffer into the resampler.
    spx_uint32_t in_len = ResponseFrameSize;
    spx_uint32_t out_len = resampled.Length();
    RESAMPLER_PROCESS(resampler, 0, response, &in_len, resampled.Elements(),
                      &out_len);

    if (out_len < resampled.Length()) {
      // The input should have all been processed.
      MOZ_ASSERT(in_len == ResponseFrameSize);
      // Feed in zeros get the data remaining in the resampler.
      spx_uint32_t out_index = out_len;
      in_len = speex_resampler_get_input_latency(resampler);
      out_len = resampled.Length() - out_index;
      RESAMPLER_PROCESS(resampler, 0, nullptr, &in_len,
                        resampled.Elements() + out_index, &out_len);
      out_index += out_len;
      // There may be some uninitialized samples remaining for very low
      // sample rates.
      PodZero(resampled.Elements() + out_index, resampled.Length() - out_index);
    }

    speex_resampler_reset_mem(resampler);
  }

#ifdef MOZ_SAMPLE_TYPE_S16
  AutoTArray<float, 2 * ResponseFrameSize> floatArray;
  floatArray.SetLength(resampledResponseLength);
  float* floatResponse = floatArray.Elements();
  ConvertAudioSamples(resampledResponse, floatResponse,
                      resampledResponseLength);
#else
  float* floatResponse = resampledResponse;
#endif
#undef RESAMPLER_PROCESS

  return HRTFKernel::create(floatResponse, resampledResponseLength, sampleRate);
}

// The range of elevations for the IRCAM impulse responses varies depending on
// azimuth, but the minimum elevation appears to always be -45.
//
// Here's how it goes:
static int maxElevations[] = {
    //  Azimuth
    //
    90,  // 0
    45,  // 15
    60,  // 30
    45,  // 45
    75,  // 60
    45,  // 75
    60,  // 90
    45,  // 105
    75,  // 120
    45,  // 135
    60,  // 150
    45,  // 165
    75,  // 180
    45,  // 195
    60,  // 210
    45,  // 225
    75,  // 240
    45,  // 255
    60,  // 270
    45,  // 285
    75,  // 300
    45,  // 315
    60,  // 330
    45   //  345
};

nsReturnRef<HRTFElevation> HRTFElevation::createBuiltin(int elevation,
                                                        float sampleRate) {
  if (elevation < firstElevation ||
      elevation > firstElevation + numberOfElevations * elevationSpacing ||
      (elevation / elevationSpacing) * elevationSpacing != elevation)
    return nsReturnRef<HRTFElevation>();

  // Spacing, in degrees, between every azimuth loaded from resource.
  // Some elevations do not have data for all these intervals.
  // See maxElevations.
  static const unsigned AzimuthSpacing = 15;
  static const unsigned NumberOfRawAzimuths = 360 / AzimuthSpacing;
  static_assert(AzimuthSpacing * NumberOfRawAzimuths == 360, "Not a multiple");
  static const unsigned InterpolationFactor =
      NumberOfTotalAzimuths / NumberOfRawAzimuths;
  static_assert(
      NumberOfTotalAzimuths == NumberOfRawAzimuths * InterpolationFactor,
      "Not a multiple");

  HRTFKernelList kernelListL;
  kernelListL.SetLength(NumberOfTotalAzimuths);

  SpeexResamplerState* resampler =
      sampleRate == rawSampleRate
          ? nullptr
          : speex_resampler_init(1, rawSampleRate, sampleRate,
                                 SPEEX_RESAMPLER_QUALITY_MIN, nullptr);

  // Load convolution kernels from HRTF files.
  int interpolatedIndex = 0;
  for (unsigned rawIndex = 0; rawIndex < NumberOfRawAzimuths; ++rawIndex) {
    // Don't let elevation exceed maximum for this azimuth.
    int maxElevation = maxElevations[rawIndex];
    int actualElevation = std::min(elevation, maxElevation);

    kernelListL[interpolatedIndex] = calculateKernelForAzimuthElevation(
        rawIndex * AzimuthSpacing, actualElevation, resampler, sampleRate);

    interpolatedIndex += InterpolationFactor;
  }

  if (resampler) speex_resampler_destroy(resampler);

  // Now go back and interpolate intermediate azimuth values.
  for (unsigned i = 0; i < NumberOfTotalAzimuths; i += InterpolationFactor) {
    int j = (i + InterpolationFactor) % NumberOfTotalAzimuths;

    // Create the interpolated convolution kernels and delays.
    for (unsigned jj = 1; jj < InterpolationFactor; ++jj) {
      float x =
          float(jj) / float(InterpolationFactor);  // interpolate from 0 -> 1

      kernelListL[i + jj] = HRTFKernel::createInterpolatedKernel(
          kernelListL[i], kernelListL[j], x);
    }
  }

  return nsReturnRef<HRTFElevation>(
      new HRTFElevation(std::move(kernelListL), elevation, sampleRate));
}

nsReturnRef<HRTFElevation> HRTFElevation::createByInterpolatingSlices(
    HRTFElevation* hrtfElevation1, HRTFElevation* hrtfElevation2, float x,
    float sampleRate) {
  MOZ_ASSERT(hrtfElevation1 && hrtfElevation2);
  if (!hrtfElevation1 || !hrtfElevation2) return nsReturnRef<HRTFElevation>();

  MOZ_ASSERT(x >= 0.0 && x < 1.0);

  HRTFKernelList kernelListL;
  kernelListL.SetLength(NumberOfTotalAzimuths);

  const HRTFKernelList& kernelListL1 = hrtfElevation1->kernelListL();
  const HRTFKernelList& kernelListL2 = hrtfElevation2->kernelListL();

  // Interpolate kernels of corresponding azimuths of the two elevations.
  for (unsigned i = 0; i < NumberOfTotalAzimuths; ++i) {
    kernelListL[i] = HRTFKernel::createInterpolatedKernel(kernelListL1[i],
                                                          kernelListL2[i], x);
  }

  // Interpolate elevation angle.
  double angle = (1.0 - x) * hrtfElevation1->elevationAngle() +
                 x * hrtfElevation2->elevationAngle();

  return nsReturnRef<HRTFElevation>(new HRTFElevation(
      std::move(kernelListL), static_cast<int>(angle), sampleRate));
}

void HRTFElevation::getKernelsFromAzimuth(
    double azimuthBlend, unsigned azimuthIndex, HRTFKernel*& kernelL,
    HRTFKernel*& kernelR, double& frameDelayL, double& frameDelayR) {
  bool checkAzimuthBlend = azimuthBlend >= 0.0 && azimuthBlend < 1.0;
  MOZ_ASSERT(checkAzimuthBlend);
  if (!checkAzimuthBlend) azimuthBlend = 0.0;

  unsigned numKernels = m_kernelListL.Length();

  bool isIndexGood = azimuthIndex < numKernels;
  MOZ_ASSERT(isIndexGood);
  if (!isIndexGood) {
    kernelL = 0;
    kernelR = 0;
    return;
  }

  // Return the left and right kernels,
  // using symmetry to produce the right kernel.
  kernelL = m_kernelListL[azimuthIndex];
  int azimuthIndexR = (numKernels - azimuthIndex) % numKernels;
  kernelR = m_kernelListL[azimuthIndexR];

  frameDelayL = kernelL->frameDelay();
  frameDelayR = kernelR->frameDelay();

  int azimuthIndex2L = (azimuthIndex + 1) % numKernels;
  double frameDelay2L = m_kernelListL[azimuthIndex2L]->frameDelay();
  int azimuthIndex2R = (numKernels - azimuthIndex2L) % numKernels;
  double frameDelay2R = m_kernelListL[azimuthIndex2R]->frameDelay();

  // Linearly interpolate delays.
  frameDelayL =
      (1.0 - azimuthBlend) * frameDelayL + azimuthBlend * frameDelay2L;
  frameDelayR =
      (1.0 - azimuthBlend) * frameDelayR + azimuthBlend * frameDelay2R;
}

}  // namespace WebCore