/src/mozilla-central/dom/media/webaudio/blink/HRTFElevation.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (C) 2010 Google Inc. All rights reserved. |
3 | | * |
4 | | * Redistribution and use in source and binary forms, with or without |
5 | | * modification, are permitted provided that the following conditions |
6 | | * are met: |
7 | | * |
8 | | * 1. Redistributions of source code must retain the above copyright |
9 | | * notice, this list of conditions and the following disclaimer. |
10 | | * 2. Redistributions in binary form must reproduce the above copyright |
11 | | * notice, this list of conditions and the following disclaimer in the |
12 | | * documentation and/or other materials provided with the distribution. |
13 | | * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
14 | | * its contributors may be used to endorse or promote products derived |
15 | | * from this software without specific prior written permission. |
16 | | * |
17 | | * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
18 | | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
19 | | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
20 | | * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
21 | | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
22 | | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
23 | | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
24 | | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
25 | | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
26 | | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
27 | | */ |
28 | | |
29 | | #include "HRTFElevation.h" |
30 | | |
31 | | #include <speex/speex_resampler.h> |
32 | | #include "mozilla/PodOperations.h" |
33 | | #include "AudioSampleFormat.h" |
34 | | |
35 | | #include "IRC_Composite_C_R0195-incl.cpp" |
36 | | |
37 | | using namespace std; |
38 | | using namespace mozilla; |
39 | | |
40 | | namespace WebCore { |
41 | | |
42 | | const int elevationSpacing = irc_composite_c_r0195_elevation_interval; |
43 | | const int firstElevation = irc_composite_c_r0195_first_elevation; |
44 | | const int numberOfElevations = MOZ_ARRAY_LENGTH(irc_composite_c_r0195); |
45 | | |
46 | | const unsigned HRTFElevation::NumberOfTotalAzimuths = 360 / 15 * 8; |
47 | | |
48 | | const int rawSampleRate = irc_composite_c_r0195_sample_rate; |
49 | | |
50 | | // Number of frames in an individual impulse response. |
51 | | const size_t ResponseFrameSize = 256; |
52 | | |
53 | | size_t HRTFElevation::sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const |
54 | 0 | { |
55 | 0 | size_t amount = aMallocSizeOf(this); |
56 | 0 |
|
57 | 0 | amount += m_kernelListL.ShallowSizeOfExcludingThis(aMallocSizeOf); |
58 | 0 | for (size_t i = 0; i < m_kernelListL.Length(); i++) { |
59 | 0 | amount += m_kernelListL[i]->sizeOfIncludingThis(aMallocSizeOf); |
60 | 0 | } |
61 | 0 |
|
62 | 0 | return amount; |
63 | 0 | } |
64 | | |
65 | | size_t HRTFElevation::fftSizeForSampleRate(float sampleRate) |
66 | 0 | { |
67 | 0 | // The IRCAM HRTF impulse responses were 512 sample-frames @44.1KHz, |
68 | 0 | // but these have been truncated to 256 samples. |
69 | 0 | // An FFT-size of twice impulse response size is used (for convolution). |
70 | 0 | // So for sample rates of 44.1KHz an FFT size of 512 is good. |
71 | 0 | // We double the FFT-size only for sample rates at least double this. |
72 | 0 | // If the FFT size is too large then the impulse response will be padded |
73 | 0 | // with zeros without the fade-out provided by HRTFKernel. |
74 | 0 | MOZ_ASSERT(sampleRate > 1.0 && sampleRate < 1048576.0); |
75 | 0 |
|
76 | 0 | // This is the size if we were to use all raw response samples. |
77 | 0 | unsigned resampledLength = |
78 | 0 | floorf(ResponseFrameSize * sampleRate / rawSampleRate); |
79 | 0 | // Keep things semi-sane, with max FFT size of 1024. |
80 | 0 | unsigned size = min(resampledLength, 1023U); |
81 | 0 | // Ensure a minimum of 2 * WEBAUDIO_BLOCK_SIZE (with the size++ below) for |
82 | 0 | // FFTConvolver and set the 8 least significant bits for rounding up to |
83 | 0 | // the next power of 2 below. |
84 | 0 | size |= 2 * WEBAUDIO_BLOCK_SIZE - 1; |
85 | 0 | // Round up to the next power of 2, making the FFT size no more than twice |
86 | 0 | // the impulse response length. This doubles size for values that are |
87 | 0 | // already powers of 2. This works by filling in alls bit to right of the |
88 | 0 | // most significant bit. The most significant bit is no greater than |
89 | 0 | // 1 << 9, and the least significant 8 bits were already set above, so |
90 | 0 | // there is at most one bit to add. |
91 | 0 | size |= (size >> 1); |
92 | 0 | size++; |
93 | 0 | MOZ_ASSERT((size & (size - 1)) == 0); |
94 | 0 |
|
95 | 0 | return size; |
96 | 0 | } |
97 | | |
98 | | nsReturnRef<HRTFKernel> HRTFElevation::calculateKernelForAzimuthElevation(int azimuth, int elevation, SpeexResamplerState* resampler, float sampleRate) |
99 | 0 | { |
100 | 0 | int elevationIndex = (elevation - firstElevation) / elevationSpacing; |
101 | 0 | MOZ_ASSERT(elevationIndex >= 0 && elevationIndex <= numberOfElevations); |
102 | 0 |
|
103 | 0 | int numberOfAzimuths = irc_composite_c_r0195[elevationIndex].count; |
104 | 0 | int azimuthSpacing = 360 / numberOfAzimuths; |
105 | 0 | MOZ_ASSERT(numberOfAzimuths * azimuthSpacing == 360); |
106 | 0 |
|
107 | 0 | int azimuthIndex = azimuth / azimuthSpacing; |
108 | 0 | MOZ_ASSERT(azimuthIndex * azimuthSpacing == azimuth); |
109 | 0 |
|
110 | 0 | const int16_t (&impulse_response_data)[ResponseFrameSize] = |
111 | 0 | irc_composite_c_r0195[elevationIndex].azimuths[azimuthIndex]; |
112 | 0 |
|
113 | 0 | // When libspeex_resampler is compiled with FIXED_POINT, samples in |
114 | 0 | // speex_resampler_process_float are rounded directly to int16_t, which |
115 | 0 | // only works well if the floats are in the range +/-32767. On such |
116 | 0 | // platforms it's better to resample before converting to float anyway. |
117 | | #ifdef MOZ_SAMPLE_TYPE_S16 |
118 | | # define RESAMPLER_PROCESS speex_resampler_process_int |
119 | | const int16_t* response = impulse_response_data; |
120 | | const int16_t* resampledResponse; |
121 | | #else |
122 | 0 | # define RESAMPLER_PROCESS speex_resampler_process_float |
123 | 0 | float response[ResponseFrameSize]; |
124 | 0 | ConvertAudioSamples(impulse_response_data, response, ResponseFrameSize); |
125 | 0 | float* resampledResponse; |
126 | 0 | #endif |
127 | 0 |
|
128 | 0 | // Note that depending on the fftSize returned by the panner, we may be truncating the impulse response. |
129 | 0 | const size_t resampledResponseLength = fftSizeForSampleRate(sampleRate) / 2; |
130 | 0 |
|
131 | 0 | AutoTArray<AudioDataValue, 2 * ResponseFrameSize> resampled; |
132 | 0 | if (sampleRate == rawSampleRate) { |
133 | 0 | resampledResponse = response; |
134 | 0 | MOZ_ASSERT(resampledResponseLength == ResponseFrameSize); |
135 | 0 | } else { |
136 | 0 | resampled.SetLength(resampledResponseLength); |
137 | 0 | resampledResponse = resampled.Elements(); |
138 | 0 | speex_resampler_skip_zeros(resampler); |
139 | 0 |
|
140 | 0 | // Feed the input buffer into the resampler. |
141 | 0 | spx_uint32_t in_len = ResponseFrameSize; |
142 | 0 | spx_uint32_t out_len = resampled.Length(); |
143 | 0 | RESAMPLER_PROCESS(resampler, 0, response, &in_len, |
144 | 0 | resampled.Elements(), &out_len); |
145 | 0 |
|
146 | 0 | if (out_len < resampled.Length()) { |
147 | 0 | // The input should have all been processed. |
148 | 0 | MOZ_ASSERT(in_len == ResponseFrameSize); |
149 | 0 | // Feed in zeros get the data remaining in the resampler. |
150 | 0 | spx_uint32_t out_index = out_len; |
151 | 0 | in_len = speex_resampler_get_input_latency(resampler); |
152 | 0 | out_len = resampled.Length() - out_index; |
153 | 0 | RESAMPLER_PROCESS(resampler, 0, nullptr, &in_len, |
154 | 0 | resampled.Elements() + out_index, &out_len); |
155 | 0 | out_index += out_len; |
156 | 0 | // There may be some uninitialized samples remaining for very low |
157 | 0 | // sample rates. |
158 | 0 | PodZero(resampled.Elements() + out_index, |
159 | 0 | resampled.Length() - out_index); |
160 | 0 | } |
161 | 0 |
|
162 | 0 | speex_resampler_reset_mem(resampler); |
163 | 0 | } |
164 | 0 |
|
165 | | #ifdef MOZ_SAMPLE_TYPE_S16 |
166 | | AutoTArray<float, 2 * ResponseFrameSize> floatArray; |
167 | | floatArray.SetLength(resampledResponseLength); |
168 | | float *floatResponse = floatArray.Elements(); |
169 | | ConvertAudioSamples(resampledResponse, |
170 | | floatResponse, resampledResponseLength); |
171 | | #else |
172 | | float *floatResponse = resampledResponse; |
173 | 0 | #endif |
174 | 0 | #undef RESAMPLER_PROCESS |
175 | 0 |
|
176 | 0 | return HRTFKernel::create(floatResponse, resampledResponseLength, sampleRate); |
177 | 0 | } |
178 | | |
179 | | // The range of elevations for the IRCAM impulse responses varies depending on azimuth, but the minimum elevation appears to always be -45. |
180 | | // |
181 | | // Here's how it goes: |
182 | | static int maxElevations[] = { |
183 | | // Azimuth |
184 | | // |
185 | | 90, // 0 |
186 | | 45, // 15 |
187 | | 60, // 30 |
188 | | 45, // 45 |
189 | | 75, // 60 |
190 | | 45, // 75 |
191 | | 60, // 90 |
192 | | 45, // 105 |
193 | | 75, // 120 |
194 | | 45, // 135 |
195 | | 60, // 150 |
196 | | 45, // 165 |
197 | | 75, // 180 |
198 | | 45, // 195 |
199 | | 60, // 210 |
200 | | 45, // 225 |
201 | | 75, // 240 |
202 | | 45, // 255 |
203 | | 60, // 270 |
204 | | 45, // 285 |
205 | | 75, // 300 |
206 | | 45, // 315 |
207 | | 60, // 330 |
208 | | 45 // 345 |
209 | | }; |
210 | | |
211 | | nsReturnRef<HRTFElevation> HRTFElevation::createBuiltin(int elevation, float sampleRate) |
212 | 0 | { |
213 | 0 | if (elevation < firstElevation || |
214 | 0 | elevation > firstElevation + numberOfElevations * elevationSpacing || |
215 | 0 | (elevation / elevationSpacing) * elevationSpacing != elevation) |
216 | 0 | return nsReturnRef<HRTFElevation>(); |
217 | 0 | |
218 | 0 | // Spacing, in degrees, between every azimuth loaded from resource. |
219 | 0 | // Some elevations do not have data for all these intervals. |
220 | 0 | // See maxElevations. |
221 | 0 | static const unsigned AzimuthSpacing = 15; |
222 | 0 | static const unsigned NumberOfRawAzimuths = 360 / AzimuthSpacing; |
223 | 0 | static_assert(AzimuthSpacing * NumberOfRawAzimuths == 360, |
224 | 0 | "Not a multiple"); |
225 | 0 | static const unsigned InterpolationFactor = |
226 | 0 | NumberOfTotalAzimuths / NumberOfRawAzimuths; |
227 | 0 | static_assert(NumberOfTotalAzimuths == |
228 | 0 | NumberOfRawAzimuths * InterpolationFactor, "Not a multiple"); |
229 | 0 |
|
230 | 0 | HRTFKernelList kernelListL; |
231 | 0 | kernelListL.SetLength(NumberOfTotalAzimuths); |
232 | 0 |
|
233 | 0 | SpeexResamplerState* resampler = sampleRate == rawSampleRate ? nullptr : |
234 | 0 | speex_resampler_init(1, rawSampleRate, sampleRate, |
235 | 0 | SPEEX_RESAMPLER_QUALITY_MIN, nullptr); |
236 | 0 |
|
237 | 0 | // Load convolution kernels from HRTF files. |
238 | 0 | int interpolatedIndex = 0; |
239 | 0 | for (unsigned rawIndex = 0; rawIndex < NumberOfRawAzimuths; ++rawIndex) { |
240 | 0 | // Don't let elevation exceed maximum for this azimuth. |
241 | 0 | int maxElevation = maxElevations[rawIndex]; |
242 | 0 | int actualElevation = min(elevation, maxElevation); |
243 | 0 |
|
244 | 0 | kernelListL[interpolatedIndex] = calculateKernelForAzimuthElevation(rawIndex * AzimuthSpacing, actualElevation, resampler, sampleRate); |
245 | 0 |
|
246 | 0 | interpolatedIndex += InterpolationFactor; |
247 | 0 | } |
248 | 0 |
|
249 | 0 | if (resampler) |
250 | 0 | speex_resampler_destroy(resampler); |
251 | 0 |
|
252 | 0 | // Now go back and interpolate intermediate azimuth values. |
253 | 0 | for (unsigned i = 0; i < NumberOfTotalAzimuths; i += InterpolationFactor) { |
254 | 0 | int j = (i + InterpolationFactor) % NumberOfTotalAzimuths; |
255 | 0 |
|
256 | 0 | // Create the interpolated convolution kernels and delays. |
257 | 0 | for (unsigned jj = 1; jj < InterpolationFactor; ++jj) { |
258 | 0 | float x = float(jj) / float(InterpolationFactor); // interpolate from 0 -> 1 |
259 | 0 |
|
260 | 0 | kernelListL[i + jj] = HRTFKernel::createInterpolatedKernel(kernelListL[i], kernelListL[j], x); |
261 | 0 | } |
262 | 0 | } |
263 | 0 |
|
264 | 0 | return nsReturnRef<HRTFElevation>(new HRTFElevation(&kernelListL, elevation, sampleRate)); |
265 | 0 | } |
266 | | |
267 | | nsReturnRef<HRTFElevation> HRTFElevation::createByInterpolatingSlices(HRTFElevation* hrtfElevation1, HRTFElevation* hrtfElevation2, float x, float sampleRate) |
268 | 0 | { |
269 | 0 | MOZ_ASSERT(hrtfElevation1 && hrtfElevation2); |
270 | 0 | if (!hrtfElevation1 || !hrtfElevation2) |
271 | 0 | return nsReturnRef<HRTFElevation>(); |
272 | 0 | |
273 | 0 | MOZ_ASSERT(x >= 0.0 && x < 1.0); |
274 | 0 |
|
275 | 0 | HRTFKernelList kernelListL; |
276 | 0 | kernelListL.SetLength(NumberOfTotalAzimuths); |
277 | 0 |
|
278 | 0 | const HRTFKernelList& kernelListL1 = hrtfElevation1->kernelListL(); |
279 | 0 | const HRTFKernelList& kernelListL2 = hrtfElevation2->kernelListL(); |
280 | 0 |
|
281 | 0 | // Interpolate kernels of corresponding azimuths of the two elevations. |
282 | 0 | for (unsigned i = 0; i < NumberOfTotalAzimuths; ++i) { |
283 | 0 | kernelListL[i] = HRTFKernel::createInterpolatedKernel(kernelListL1[i], kernelListL2[i], x); |
284 | 0 | } |
285 | 0 |
|
286 | 0 | // Interpolate elevation angle. |
287 | 0 | double angle = (1.0 - x) * hrtfElevation1->elevationAngle() + x * hrtfElevation2->elevationAngle(); |
288 | 0 |
|
289 | 0 | return nsReturnRef<HRTFElevation>(new HRTFElevation(&kernelListL, static_cast<int>(angle), sampleRate)); |
290 | 0 | } |
291 | | |
292 | | void HRTFElevation::getKernelsFromAzimuth(double azimuthBlend, unsigned azimuthIndex, HRTFKernel* &kernelL, HRTFKernel* &kernelR, double& frameDelayL, double& frameDelayR) |
293 | 0 | { |
294 | 0 | bool checkAzimuthBlend = azimuthBlend >= 0.0 && azimuthBlend < 1.0; |
295 | 0 | MOZ_ASSERT(checkAzimuthBlend); |
296 | 0 | if (!checkAzimuthBlend) |
297 | 0 | azimuthBlend = 0.0; |
298 | 0 |
|
299 | 0 | unsigned numKernels = m_kernelListL.Length(); |
300 | 0 |
|
301 | 0 | bool isIndexGood = azimuthIndex < numKernels; |
302 | 0 | MOZ_ASSERT(isIndexGood); |
303 | 0 | if (!isIndexGood) { |
304 | 0 | kernelL = 0; |
305 | 0 | kernelR = 0; |
306 | 0 | return; |
307 | 0 | } |
308 | 0 | |
309 | 0 | // Return the left and right kernels, |
310 | 0 | // using symmetry to produce the right kernel. |
311 | 0 | kernelL = m_kernelListL[azimuthIndex]; |
312 | 0 | int azimuthIndexR = (numKernels - azimuthIndex) % numKernels; |
313 | 0 | kernelR = m_kernelListL[azimuthIndexR]; |
314 | 0 |
|
315 | 0 | frameDelayL = kernelL->frameDelay(); |
316 | 0 | frameDelayR = kernelR->frameDelay(); |
317 | 0 |
|
318 | 0 | int azimuthIndex2L = (azimuthIndex + 1) % numKernels; |
319 | 0 | double frameDelay2L = m_kernelListL[azimuthIndex2L]->frameDelay(); |
320 | 0 | int azimuthIndex2R = (numKernels - azimuthIndex2L) % numKernels; |
321 | 0 | double frameDelay2R = m_kernelListL[azimuthIndex2R]->frameDelay(); |
322 | 0 |
|
323 | 0 | // Linearly interpolate delays. |
324 | 0 | frameDelayL = (1.0 - azimuthBlend) * frameDelayL + azimuthBlend * frameDelay2L; |
325 | 0 | frameDelayR = (1.0 - azimuthBlend) * frameDelayR + azimuthBlend * frameDelay2R; |
326 | 0 | } |
327 | | |
328 | | } // namespace WebCore |