Check that AltiVec is enabled before using it
[openal-soft.git] / utils / makemhr / makemhr.cpp
blob1c88259612f6f8e69ac65859b9dc8e017016145e
1 /*
2 * HRTF utility for producing and demonstrating the process of creating an
3 * OpenAL Soft compatible HRIR data set.
5 * Copyright (C) 2011-2019 Christopher Fitzgerald
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 * Or visit: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
23 * --------------------------------------------------------------------------
25 * A big thanks goes out to all those whose work done in the field of
26 * binaural sound synthesis using measured HRTFs makes this utility and the
27 * OpenAL Soft implementation possible.
29 * The algorithm for diffuse-field equalization was adapted from the work
30 * done by Rio Emmanuel and Larcher Veronique of IRCAM and Bill Gardner of
31 * MIT Media Laboratory. It operates as follows:
33 * 1. Take the FFT of each HRIR and only keep the magnitude responses.
34 * 2. Calculate the diffuse-field power-average of all HRIRs weighted by
35 * their contribution to the total surface area covered by their
36 * measurement. This has since been modified to use coverage volume for
37 * multi-field HRIR data sets.
38 * 3. Take the diffuse-field average and limit its magnitude range.
39 * 4. Equalize the responses by using the inverse of the diffuse-field
40 * average.
41 * 5. Reconstruct the minimum-phase responses.
42 * 5. Zero the DC component.
43 * 6. IFFT the result and truncate to the desired-length minimum-phase FIR.
45 * The spherical head algorithm for calculating propagation delay was adapted
46 * from the paper:
48 * Modeling Interaural Time Difference Assuming a Spherical Head
49 * Joel David Miller
50 * Music 150, Musical Acoustics, Stanford University
51 * December 2, 2001
53 * The formulae for calculating the Kaiser window metrics are from the
54 * the textbook:
56 * Discrete-Time Signal Processing
57 * Alan V. Oppenheim and Ronald W. Schafer
58 * Prentice-Hall Signal Processing Series
59 * 1999
62 #define _UNICODE /* NOLINT(bugprone-reserved-identifier) */
63 #include "config.h"
65 #include "makemhr.h"
67 #include <algorithm>
68 #include <atomic>
69 #include <chrono>
70 #include <cmath>
71 #include <complex>
72 #include <cstdint>
73 #include <cstdio>
74 #include <cstdlib>
75 #include <cstring>
76 #include <filesystem>
77 #include <fstream>
78 #include <functional>
79 #include <iostream>
80 #include <limits>
81 #include <memory>
82 #include <numeric>
83 #include <string_view>
84 #include <thread>
85 #include <utility>
86 #include <vector>
88 #include "alcomplex.h"
89 #include "alnumbers.h"
90 #include "alnumeric.h"
91 #include "alspan.h"
92 #include "alstring.h"
93 #include "loaddef.h"
94 #include "loadsofa.h"
96 #include "win_main_utf8.h"
99 HrirDataT::~HrirDataT() = default;
101 namespace {
103 using namespace std::string_view_literals;
105 struct FileDeleter {
106 void operator()(gsl::owner<FILE*> f) { fclose(f); }
108 using FilePtr = std::unique_ptr<FILE,FileDeleter>;
110 // The epsilon used to maintain signal stability.
111 constexpr double Epsilon{1e-9};
113 // The limits to the FFT window size override on the command line.
114 constexpr uint MinFftSize{65536};
115 constexpr uint MaxFftSize{131072};
117 // The limits to the equalization range limit on the command line.
118 constexpr double MinLimit{2.0};
119 constexpr double MaxLimit{120.0};
121 // The limits to the truncation window size on the command line.
122 constexpr uint MinTruncSize{16};
123 constexpr uint MaxTruncSize{128};
125 // The limits to the custom head radius on the command line.
126 constexpr double MinCustomRadius{0.05};
127 constexpr double MaxCustomRadius{0.15};
129 // The maximum propagation delay value supported by OpenAL Soft.
130 constexpr double MaxHrtd{63.0};
132 // The OpenAL Soft HRTF format marker. It stands for minimum-phase head
133 // response protocol 03.
134 constexpr auto GetMHRMarker() noexcept { return "MinPHR03"sv; }
137 // Head model used for calculating the impulse delays.
138 enum HeadModelT {
139 HM_None,
140 HM_Dataset, // Measure the onset from the dataset.
141 HM_Sphere, // Calculate the onset using a spherical head model.
143 HM_Default = HM_Dataset
147 // The defaults for the command line options.
148 constexpr uint DefaultFftSize{65536};
149 constexpr bool DefaultEqualize{true};
150 constexpr bool DefaultSurface{true};
151 constexpr double DefaultLimit{24.0};
152 constexpr uint DefaultTruncSize{64};
153 constexpr double DefaultCustomRadius{0.0};
155 /* Channel index enums. Mono uses LeftChannel only. */
156 enum ChannelIndex : uint {
157 LeftChannel = 0u,
158 RightChannel = 1u
162 /* Performs a string substitution. Any case-insensitive occurrences of the
163 * pattern string are replaced with the replacement string. The result is
164 * truncated if necessary.
166 auto StrSubst(std::string_view in, const std::string_view pat, const std::string_view rep) -> std::string
168 std::string ret;
169 ret.reserve(in.size() + pat.size());
171 while(in.size() >= pat.size())
173 if(al::starts_with(in, pat))
175 in = in.substr(pat.size());
176 ret += rep;
178 else
180 size_t endpos{1};
181 while(endpos < in.size() && std::toupper(in[endpos]) != std::toupper(pat.front()))
182 ++endpos;
183 ret += in.substr(0, endpos);
184 in = in.substr(endpos);
187 ret += in;
189 return ret;
193 /*********************
194 *** Math routines ***
195 *********************/
197 // Simple clamp routine.
198 double Clamp(const double val, const double lower, const double upper)
200 return std::min(std::max(val, lower), upper);
203 inline uint dither_rng(uint *seed)
205 *seed = *seed * 96314165 + 907633515;
206 return *seed;
209 // Performs a triangular probability density function dither. The input samples
210 // should be normalized (-1 to +1).
211 void TpdfDither(const al::span<double> out, const al::span<const double> in, const double scale,
212 const size_t channel, const size_t step, uint *seed)
214 static constexpr double PRNG_SCALE = 1.0 / std::numeric_limits<uint>::max();
215 assert(channel < step);
217 for(size_t i{0};i < in.size();++i)
219 uint prn0{dither_rng(seed)};
220 uint prn1{dither_rng(seed)};
221 out[i*step + channel] = std::round(in[i]*scale + (prn0*PRNG_SCALE - prn1*PRNG_SCALE));
225 /* Apply a range limit (in dB) to the given magnitude response. This is used
226 * to adjust the effects of the diffuse-field average on the equalization
227 * process.
229 void LimitMagnitudeResponse(const uint n, const uint m, const double limit,
230 const al::span<double> inout)
232 const double halfLim{limit / 2.0};
233 // Convert the response to dB.
234 for(uint i{0};i < m;++i)
235 inout[i] = 20.0 * std::log10(inout[i]);
236 // Use six octaves to calculate the average magnitude of the signal.
237 const auto lower = (static_cast<uint>(std::ceil(n / std::pow(2.0, 8.0)))) - 1;
238 const auto upper = (static_cast<uint>(std::floor(n / std::pow(2.0, 2.0)))) - 1;
239 double ave{0.0};
240 for(uint i{lower};i <= upper;++i)
241 ave += inout[i];
242 ave /= upper - lower + 1;
243 // Keep the response within range of the average magnitude.
244 for(uint i{0};i < m;++i)
245 inout[i] = Clamp(inout[i], ave - halfLim, ave + halfLim);
246 // Convert the response back to linear magnitude.
247 for(uint i{0};i < m;++i)
248 inout[i] = std::pow(10.0, inout[i] / 20.0);
251 /* Reconstructs the minimum-phase component for the given magnitude response
252 * of a signal. This is equivalent to phase recomposition, sans the missing
253 * residuals (which were discarded). The mirrored half of the response is
254 * reconstructed.
256 void MinimumPhase(const al::span<double> mags, const al::span<complex_d> out)
258 assert(mags.size() == out.size());
259 const size_t m{(mags.size()/2) + 1};
261 size_t i;
262 for(i = 0;i < m;i++)
263 out[i] = std::log(mags[i]);
264 for(;i < mags.size();++i)
266 mags[i] = mags[mags.size() - i];
267 out[i] = out[mags.size() - i];
269 complex_hilbert(out);
270 // Remove any DC offset the filter has.
271 mags[0] = Epsilon;
272 for(i = 0;i < mags.size();++i)
273 out[i] = std::polar(mags[i], out[i].imag());
277 /***************************
278 *** File storage output ***
279 ***************************/
281 // Write an ASCII string to a file.
282 auto WriteAscii(const std::string_view out, std::ostream &ostream, const std::string_view filename) -> int
284 if(!ostream.write(out.data(), std::streamsize(out.size())) || ostream.bad())
286 fprintf(stderr, "\nError: Bad write to file '%.*s'.\n", al::sizei(filename),
287 filename.data());
288 return 0;
290 return 1;
293 // Write a binary value of the given byte order and byte size to a file,
294 // loading it from a 32-bit unsigned integer.
295 auto WriteBin4(const uint bytes, const uint32_t in, std::ostream &ostream,
296 const std::string_view filename) -> int
298 std::array<char,4> out{};
299 for(uint i{0};i < bytes;i++)
300 out[i] = static_cast<char>((in>>(i*8)) & 0x000000FF);
302 if(!ostream.write(out.data(), std::streamsize(bytes)) || ostream.bad())
304 fprintf(stderr, "\nError: Bad write to file '%.*s'.\n", al::sizei(filename),
305 filename.data());
306 return 0;
308 return 1;
311 // Store the OpenAL Soft HRTF data set.
312 auto StoreMhr(const HrirDataT *hData, const std::string_view filename) -> bool
314 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
315 const uint n{hData->mIrPoints};
316 uint dither_seed{22222};
318 auto ostream = std::ofstream{std::filesystem::u8path(filename), std::ios::binary};
319 if(!ostream.is_open())
321 fprintf(stderr, "\nError: Could not open MHR file '%.*s'.\n", al::sizei(filename),
322 filename.data());
323 return false;
325 if(!WriteAscii(GetMHRMarker(), ostream, filename))
326 return false;
327 if(!WriteBin4(4, hData->mIrRate, ostream, filename))
328 return false;
329 if(!WriteBin4(1, static_cast<uint32_t>(hData->mChannelType), ostream, filename))
330 return false;
331 if(!WriteBin4(1, hData->mIrPoints, ostream, filename))
332 return false;
333 if(!WriteBin4(1, static_cast<uint>(hData->mFds.size()), ostream, filename))
334 return false;
335 for(size_t fi{hData->mFds.size()-1};fi < hData->mFds.size();--fi)
337 auto fdist = static_cast<uint32_t>(std::round(1000.0 * hData->mFds[fi].mDistance));
338 if(!WriteBin4(2, fdist, ostream, filename))
339 return false;
340 if(!WriteBin4(1, static_cast<uint32_t>(hData->mFds[fi].mEvs.size()), ostream, filename))
341 return false;
342 for(size_t ei{0};ei < hData->mFds[fi].mEvs.size();++ei)
344 const auto &elev = hData->mFds[fi].mEvs[ei];
345 if(!WriteBin4(1, static_cast<uint32_t>(elev.mAzs.size()), ostream, filename))
346 return false;
350 for(size_t fi{hData->mFds.size()-1};fi < hData->mFds.size();--fi)
352 static constexpr double scale{8388607.0};
353 static constexpr uint bps{3u};
355 for(const auto &evd : hData->mFds[fi].mEvs)
357 for(const auto &azd : evd.mAzs)
359 std::array<double,MaxTruncSize*2_uz> out{};
361 TpdfDither(out, azd.mIrs[0].first(n), scale, 0, channels, &dither_seed);
362 if(hData->mChannelType == CT_STEREO)
363 TpdfDither(out, azd.mIrs[1].first(n), scale, 1, channels, &dither_seed);
364 const size_t numsamples{size_t{channels} * n};
365 for(size_t i{0};i < numsamples;i++)
367 const auto v = static_cast<int>(Clamp(out[i], -scale-1.0, scale));
368 if(!WriteBin4(bps, static_cast<uint32_t>(v), ostream, filename))
369 return false;
374 for(size_t fi{hData->mFds.size()-1};fi < hData->mFds.size();--fi)
376 /* Delay storage has 2 bits of extra precision. */
377 static constexpr double DelayPrecScale{4.0};
378 for(const auto &evd : hData->mFds[fi].mEvs)
380 for(const auto &azd : evd.mAzs)
382 auto v = static_cast<uint>(std::round(azd.mDelays[0]*DelayPrecScale));
383 if(!WriteBin4(1, v, ostream, filename)) return false;
384 if(hData->mChannelType == CT_STEREO)
386 v = static_cast<uint>(std::round(azd.mDelays[1]*DelayPrecScale));
387 if(!WriteBin4(1, v, ostream, filename)) return false;
392 return true;
396 /***********************
397 *** HRTF processing ***
398 ***********************/
400 /* Balances the maximum HRIR magnitudes of multi-field data sets by
401 * independently normalizing each field in relation to the overall maximum.
402 * This is done to ignore distance attenuation.
404 void BalanceFieldMagnitudes(const HrirDataT *hData, const uint channels, const uint m)
406 std::array<double,MAX_FD_COUNT> maxMags{};
407 double maxMag{0.0};
409 for(size_t fi{0};fi < hData->mFds.size();++fi)
411 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
413 for(const auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
415 for(size_t ti{0};ti < channels;++ti)
417 for(size_t i{0};i < m;++i)
418 maxMags[fi] = std::max(azd.mIrs[ti][i], maxMags[fi]);
423 maxMag = std::max(maxMags[fi], maxMag);
426 for(size_t fi{0};fi < hData->mFds.size();++fi)
428 const double magFactor{maxMag / maxMags[fi]};
430 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
432 for(const auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
434 for(size_t ti{0};ti < channels;++ti)
436 for(size_t i{0};i < m;++i)
437 azd.mIrs[ti][i] *= magFactor;
444 /* Calculate the contribution of each HRIR to the diffuse-field average based
445 * on its coverage volume. All volumes are centered at the spherical HRIR
446 * coordinates and measured by extruded solid angle.
448 void CalculateDfWeights(const HrirDataT *hData, const al::span<double> weights)
450 double sum, innerRa, outerRa, evs, ev, upperEv, lowerEv;
451 double solidAngle, solidVolume;
452 uint fi, ei;
454 sum = 0.0;
455 // The head radius acts as the limit for the inner radius.
456 innerRa = hData->mRadius;
457 for(fi = 0;fi < hData->mFds.size();fi++)
459 // Each volume ends half way between progressive field measurements.
460 if((fi + 1) < hData->mFds.size())
461 outerRa = 0.5f * (hData->mFds[fi].mDistance + hData->mFds[fi + 1].mDistance);
462 // The final volume has its limit extended to some practical value.
463 // This is done to emphasize the far-field responses in the average.
464 else
465 outerRa = 10.0f;
467 const double raPowDiff{std::pow(outerRa, 3.0) - std::pow(innerRa, 3.0)};
468 evs = al::numbers::pi / 2.0 / static_cast<double>(hData->mFds[fi].mEvs.size() - 1);
469 for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
471 const auto &elev = hData->mFds[fi].mEvs[ei];
472 // For each elevation, calculate the upper and lower limits of
473 // the patch band.
474 ev = elev.mElevation;
475 lowerEv = std::max(-al::numbers::pi / 2.0, ev - evs);
476 upperEv = std::min(al::numbers::pi / 2.0, ev + evs);
477 // Calculate the surface area of the patch band.
478 solidAngle = 2.0 * al::numbers::pi * (std::sin(upperEv) - std::sin(lowerEv));
479 // Then the volume of the extruded patch band.
480 solidVolume = solidAngle * raPowDiff / 3.0;
481 // Each weight is the volume of one extruded patch.
482 weights[(fi*MAX_EV_COUNT) + ei] = solidVolume / static_cast<double>(elev.mAzs.size());
483 // Sum the total coverage volume of the HRIRs for all fields.
484 sum += solidAngle;
487 innerRa = outerRa;
490 for(fi = 0;fi < hData->mFds.size();fi++)
492 // Normalize the weights given the total surface coverage for all
493 // fields.
494 for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
495 weights[(fi * MAX_EV_COUNT) + ei] /= sum;
499 /* Calculate the diffuse-field average from the given magnitude responses of
500 * the HRIR set. Weighting can be applied to compensate for the varying
501 * coverage of each HRIR. The final average can then be limited by the
502 * specified magnitude range (in positive dB; 0.0 to skip).
504 void CalculateDiffuseFieldAverage(const HrirDataT *hData, const uint channels, const uint m,
505 const bool weighted, const double limit, const al::span<double> dfa)
507 std::vector<double> weights(hData->mFds.size() * MAX_EV_COUNT);
508 uint count;
510 if(weighted)
512 // Use coverage weighting to calculate the average.
513 CalculateDfWeights(hData, weights);
515 else
517 double weight;
519 // If coverage weighting is not used, the weights still need to be
520 // averaged by the number of existing HRIRs.
521 count = hData->mIrCount;
522 for(size_t fi{0};fi < hData->mFds.size();++fi)
524 for(size_t ei{0};ei < hData->mFds[fi].mEvStart;++ei)
525 count -= static_cast<uint>(hData->mFds[fi].mEvs[ei].mAzs.size());
527 weight = 1.0 / count;
529 for(size_t fi{0};fi < hData->mFds.size();++fi)
531 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
532 weights[(fi * MAX_EV_COUNT) + ei] = weight;
535 for(size_t ti{0};ti < channels;++ti)
537 for(size_t i{0};i < m;++i)
538 dfa[(ti * m) + i] = 0.0;
539 for(size_t fi{0};fi < hData->mFds.size();++fi)
541 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
543 for(size_t ai{0};ai < hData->mFds[fi].mEvs[ei].mAzs.size();++ai)
545 HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai];
546 // Get the weight for this HRIR's contribution.
547 double weight = weights[(fi * MAX_EV_COUNT) + ei];
549 // Add this HRIR's weighted power average to the total.
550 for(size_t i{0};i < m;++i)
551 dfa[(ti * m) + i] += weight * azd->mIrs[ti][i] * azd->mIrs[ti][i];
555 // Finish the average calculation and keep it from being too small.
556 for(size_t i{0};i < m;++i)
557 dfa[(ti * m) + i] = std::max(sqrt(dfa[(ti * m) + i]), Epsilon);
558 // Apply a limit to the magnitude range of the diffuse-field average
559 // if desired.
560 if(limit > 0.0)
561 LimitMagnitudeResponse(hData->mFftSize, m, limit, dfa.subspan(ti * m));
565 // Perform diffuse-field equalization on the magnitude responses of the HRIR
566 // set using the given average response.
567 void DiffuseFieldEqualize(const uint channels, const uint m, const al::span<const double> dfa,
568 const HrirDataT *hData)
570 for(size_t fi{0};fi < hData->mFds.size();++fi)
572 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
574 for(auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
576 for(size_t ti{0};ti < channels;++ti)
578 for(size_t i{0};i < m;++i)
579 azd.mIrs[ti][i] /= dfa[(ti * m) + i];
586 /* Given field and elevation indices and an azimuth, calculate the indices of
587 * the two HRIRs that bound the coordinate along with a factor for
588 * calculating the continuous HRIR using interpolation.
590 void CalcAzIndices(const HrirFdT &field, const uint ei, const double az, uint *a0, uint *a1, double *af)
592 double f{(2.0*al::numbers::pi + az) * static_cast<double>(field.mEvs[ei].mAzs.size()) /
593 (2.0*al::numbers::pi)};
594 const uint i{static_cast<uint>(f) % static_cast<uint>(field.mEvs[ei].mAzs.size())};
596 f -= std::floor(f);
597 *a0 = i;
598 *a1 = (i + 1) % static_cast<uint>(field.mEvs[ei].mAzs.size());
599 *af = f;
602 /* Synthesize any missing onset timings at the bottom elevations of each field.
603 * This just mirrors some top elevations for the bottom, and blends the
604 * remaining elevations (not an accurate model).
606 void SynthesizeOnsets(HrirDataT *hData)
608 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
610 auto proc_field = [channels](HrirFdT &field) -> void
612 /* Get the starting elevation from the measurements, and use it as the
613 * upper elevation limit for what needs to be calculated.
615 const uint upperElevReal{field.mEvStart};
616 if(upperElevReal <= 0) return;
618 /* Get the lowest half of the missing elevations' delays by mirroring
619 * the top elevation delays. The responses are on a spherical grid
620 * centered between the ears, so these should align.
622 uint ei{};
623 if(channels > 1)
625 /* Take the polar opposite position of the desired measurement and
626 * swap the ears.
628 field.mEvs[0].mAzs[0].mDelays[0] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[1];
629 field.mEvs[0].mAzs[0].mDelays[1] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[0];
630 for(ei = 1u;ei < (upperElevReal+1)/2;++ei)
632 const uint topElev{static_cast<uint>(field.mEvs.size()-ei-1)};
634 for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
636 uint a0, a1;
637 double af;
639 /* Rotate this current azimuth by a half-circle, and lookup
640 * the mirrored elevation to find the indices for the polar
641 * opposite position (may need blending).
643 const double az{field.mEvs[ei].mAzs[ai].mAzimuth + al::numbers::pi};
644 CalcAzIndices(field, topElev, az, &a0, &a1, &af);
646 /* Blend the delays, and again, swap the ears. */
647 field.mEvs[ei].mAzs[ai].mDelays[0] = Lerp(
648 field.mEvs[topElev].mAzs[a0].mDelays[1],
649 field.mEvs[topElev].mAzs[a1].mDelays[1], af);
650 field.mEvs[ei].mAzs[ai].mDelays[1] = Lerp(
651 field.mEvs[topElev].mAzs[a0].mDelays[0],
652 field.mEvs[topElev].mAzs[a1].mDelays[0], af);
656 else
658 field.mEvs[0].mAzs[0].mDelays[0] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[0];
659 for(ei = 1u;ei < (upperElevReal+1)/2;++ei)
661 const uint topElev{static_cast<uint>(field.mEvs.size()-ei-1)};
663 for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
665 uint a0, a1;
666 double af;
668 /* For mono data sets, mirror the azimuth front<->back
669 * since the other ear is a mirror of what we have (e.g.
670 * the left ear's back-left is simulated with the right
671 * ear's front-right, which uses the left ear's front-left
672 * measurement).
674 double az{field.mEvs[ei].mAzs[ai].mAzimuth};
675 if(az <= al::numbers::pi) az = al::numbers::pi - az;
676 else az = (al::numbers::pi*2.0)-az + al::numbers::pi;
677 CalcAzIndices(field, topElev, az, &a0, &a1, &af);
679 field.mEvs[ei].mAzs[ai].mDelays[0] = Lerp(
680 field.mEvs[topElev].mAzs[a0].mDelays[0],
681 field.mEvs[topElev].mAzs[a1].mDelays[0], af);
685 /* Record the lowest elevation filled in with the mirrored top. */
686 const uint lowerElevFake{ei-1u};
688 /* Fill in the remaining delays using bilinear interpolation. This
689 * helps smooth the transition back to the real delays.
691 for(;ei < upperElevReal;++ei)
693 const double ef{(field.mEvs[upperElevReal].mElevation - field.mEvs[ei].mElevation) /
694 (field.mEvs[upperElevReal].mElevation - field.mEvs[lowerElevFake].mElevation)};
696 for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
698 uint a0, a1, a2, a3;
699 double af0, af1;
701 double az{field.mEvs[ei].mAzs[ai].mAzimuth};
702 CalcAzIndices(field, upperElevReal, az, &a0, &a1, &af0);
703 CalcAzIndices(field, lowerElevFake, az, &a2, &a3, &af1);
704 std::array<double,4> blend{{
705 (1.0-ef) * (1.0-af0),
706 (1.0-ef) * ( af0),
707 ( ef) * (1.0-af1),
708 ( ef) * ( af1)
711 for(uint ti{0u};ti < channels;ti++)
713 field.mEvs[ei].mAzs[ai].mDelays[ti] =
714 field.mEvs[upperElevReal].mAzs[a0].mDelays[ti]*blend[0] +
715 field.mEvs[upperElevReal].mAzs[a1].mDelays[ti]*blend[1] +
716 field.mEvs[lowerElevFake].mAzs[a2].mDelays[ti]*blend[2] +
717 field.mEvs[lowerElevFake].mAzs[a3].mDelays[ti]*blend[3];
722 std::for_each(hData->mFds.begin(), hData->mFds.end(), proc_field);
725 /* Attempt to synthesize any missing HRIRs at the bottom elevations of each
726 * field. Right now this just blends the lowest elevation HRIRs together and
727 * applies a low-pass filter to simulate body occlusion. It is a simple, if
728 * inaccurate model.
730 void SynthesizeHrirs(HrirDataT *hData)
732 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
733 auto htemp = std::vector<complex_d>(hData->mFftSize);
734 const uint m{hData->mFftSize/2u + 1u};
735 auto filter = std::vector<double>(m);
736 const double beta{3.5e-6 * hData->mIrRate};
738 auto proc_field = [channels,m,beta,&htemp,&filter](HrirFdT &field) -> void
740 const uint oi{field.mEvStart};
741 if(oi <= 0) return;
743 for(uint ti{0u};ti < channels;ti++)
745 uint a0, a1;
746 double af;
748 /* Use the lowest immediate-left response for the left ear and
749 * lowest immediate-right response for the right ear. Given no comb
750 * effects as a result of the left response reaching the right ear
751 * and vice-versa, this produces a decent phantom-center response
752 * underneath the head.
754 CalcAzIndices(field, oi, al::numbers::pi / ((ti==0) ? -2.0 : 2.0), &a0, &a1, &af);
755 for(uint i{0u};i < m;i++)
757 field.mEvs[0].mAzs[0].mIrs[ti][i] = Lerp(field.mEvs[oi].mAzs[a0].mIrs[ti][i],
758 field.mEvs[oi].mAzs[a1].mIrs[ti][i], af);
762 for(uint ei{1u};ei < field.mEvStart;ei++)
764 const double of{static_cast<double>(ei) / field.mEvStart};
765 const double b{(1.0 - of) * beta};
766 std::array<double,4> lp{};
768 /* Calculate a low-pass filter to simulate body occlusion. */
769 lp[0] = Lerp(1.0, lp[0], b);
770 lp[1] = Lerp(lp[0], lp[1], b);
771 lp[2] = Lerp(lp[1], lp[2], b);
772 lp[3] = Lerp(lp[2], lp[3], b);
773 htemp[0] = lp[3];
774 for(size_t i{1u};i < htemp.size();i++)
776 lp[0] = Lerp(0.0, lp[0], b);
777 lp[1] = Lerp(lp[0], lp[1], b);
778 lp[2] = Lerp(lp[1], lp[2], b);
779 lp[3] = Lerp(lp[2], lp[3], b);
780 htemp[i] = lp[3];
782 /* Get the filter's frequency-domain response and extract the
783 * frequency magnitudes (phase will be reconstructed later)).
785 FftForward(static_cast<uint>(htemp.size()), htemp.data());
786 std::transform(htemp.cbegin(), htemp.cbegin()+m, filter.begin(),
787 [](const complex_d c) -> double { return std::abs(c); });
789 for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
791 uint a0, a1;
792 double af;
794 CalcAzIndices(field, oi, field.mEvs[ei].mAzs[ai].mAzimuth, &a0, &a1, &af);
795 for(uint ti{0u};ti < channels;ti++)
797 for(uint i{0u};i < m;i++)
799 /* Blend the two defined HRIRs closest to this azimuth,
800 * then blend that with the synthesized -90 elevation.
802 const double s1{Lerp(field.mEvs[oi].mAzs[a0].mIrs[ti][i],
803 field.mEvs[oi].mAzs[a1].mIrs[ti][i], af)};
804 const double s{Lerp(field.mEvs[0].mAzs[0].mIrs[ti][i], s1, of)};
805 field.mEvs[ei].mAzs[ai].mIrs[ti][i] = s * filter[i];
810 const double b{beta};
811 std::array<double,4> lp{};
812 lp[0] = Lerp(1.0, lp[0], b);
813 lp[1] = Lerp(lp[0], lp[1], b);
814 lp[2] = Lerp(lp[1], lp[2], b);
815 lp[3] = Lerp(lp[2], lp[3], b);
816 htemp[0] = lp[3];
817 for(size_t i{1u};i < htemp.size();i++)
819 lp[0] = Lerp(0.0, lp[0], b);
820 lp[1] = Lerp(lp[0], lp[1], b);
821 lp[2] = Lerp(lp[1], lp[2], b);
822 lp[3] = Lerp(lp[2], lp[3], b);
823 htemp[i] = lp[3];
825 FftForward(static_cast<uint>(htemp.size()), htemp.data());
826 std::transform(htemp.cbegin(), htemp.cbegin()+m, filter.begin(),
827 [](const complex_d c) -> double { return std::abs(c); });
829 for(uint ti{0u};ti < channels;ti++)
831 for(uint i{0u};i < m;i++)
832 field.mEvs[0].mAzs[0].mIrs[ti][i] *= filter[i];
835 std::for_each(hData->mFds.begin(), hData->mFds.end(), proc_field);
838 // The following routines assume a full set of HRIRs for all elevations.
840 /* Perform minimum-phase reconstruction using the magnitude responses of the
841 * HRIR set. Work is delegated to this struct, which runs asynchronously on one
842 * or more threads (sharing the same reconstructor object).
844 struct HrirReconstructor {
845 std::vector<al::span<double>> mIrs;
846 std::atomic<size_t> mCurrent{};
847 std::atomic<size_t> mDone{};
848 uint mFftSize{};
849 uint mIrPoints{};
851 void Worker()
853 auto h = std::vector<complex_d>(mFftSize);
854 auto mags = std::vector<double>(mFftSize);
855 size_t m{(mFftSize/2) + 1};
857 while(true)
859 /* Load the current index to process. */
860 size_t idx{mCurrent.load()};
861 do {
862 /* If the index is at the end, we're done. */
863 if(idx >= mIrs.size())
864 return;
865 /* Otherwise, increment the current index atomically so other
866 * threads know to go to the next one. If this call fails, the
867 * current index was just changed by another thread and the new
868 * value is loaded into idx, which we'll recheck.
870 } while(!mCurrent.compare_exchange_weak(idx, idx+1, std::memory_order_relaxed));
872 /* Now do the reconstruction, and apply the inverse FFT to get the
873 * time-domain response.
875 for(size_t i{0};i < m;++i)
876 mags[i] = std::max(mIrs[idx][i], Epsilon);
877 MinimumPhase(mags, h);
878 FftInverse(mFftSize, h.data());
879 for(uint i{0u};i < mIrPoints;++i)
880 mIrs[idx][i] = h[i].real();
882 /* Increment the number of IRs done. */
883 mDone.fetch_add(1);
888 void ReconstructHrirs(const HrirDataT *hData, const uint numThreads)
890 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
892 /* Set up the reconstructor with the needed size info and pointers to the
893 * IRs to process.
895 HrirReconstructor reconstructor;
896 reconstructor.mCurrent.store(0, std::memory_order_relaxed);
897 reconstructor.mDone.store(0, std::memory_order_relaxed);
898 reconstructor.mFftSize = hData->mFftSize;
899 reconstructor.mIrPoints = hData->mIrPoints;
900 for(const auto &field : hData->mFds)
902 for(auto &elev : field.mEvs)
904 for(const auto &azd : elev.mAzs)
906 for(uint ti{0u};ti < channels;ti++)
907 reconstructor.mIrs.push_back(azd.mIrs[ti]);
912 /* Launch threads to work on reconstruction. */
913 std::vector<std::thread> thrds;
914 thrds.reserve(numThreads);
915 for(size_t i{0};i < numThreads;++i)
916 thrds.emplace_back(std::mem_fn(&HrirReconstructor::Worker), &reconstructor);
918 /* Keep track of the number of IRs done, periodically reporting it. */
919 size_t count;
920 do {
921 std::this_thread::sleep_for(std::chrono::milliseconds{50});
923 count = reconstructor.mDone.load();
924 size_t pcdone{count * 100 / reconstructor.mIrs.size()};
926 printf("\r%3zu%% done (%zu of %zu)", pcdone, count, reconstructor.mIrs.size());
927 fflush(stdout);
928 } while(count < reconstructor.mIrs.size());
929 fputc('\n', stdout);
931 for(auto &thrd : thrds)
933 if(thrd.joinable())
934 thrd.join();
938 // Normalize the HRIR set and slightly attenuate the result.
939 void NormalizeHrirs(HrirDataT *hData)
941 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
942 const uint irSize{hData->mIrPoints};
944 /* Find the maximum amplitude and RMS out of all the IRs. */
945 struct LevelPair { double amp, rms; };
946 auto mesasure_channel = [irSize](const LevelPair levels, al::span<const double> ir)
948 /* Calculate the peak amplitude and RMS of this IR. */
949 ir = ir.first(irSize);
950 auto current = std::accumulate(ir.cbegin(), ir.cend(), LevelPair{0.0, 0.0},
951 [](const LevelPair cur, const double impulse)
953 return LevelPair{std::max(std::abs(impulse), cur.amp), cur.rms + impulse*impulse};
955 current.rms = std::sqrt(current.rms / irSize);
957 /* Accumulate levels by taking the maximum amplitude and RMS. */
958 return LevelPair{std::max(current.amp, levels.amp), std::max(current.rms, levels.rms)};
960 auto measure_azi = [channels,mesasure_channel](const LevelPair levels, const HrirAzT &azi)
961 { return std::accumulate(azi.mIrs.begin(), azi.mIrs.begin()+channels, levels, mesasure_channel); };
962 auto measure_elev = [measure_azi](const LevelPair levels, const HrirEvT &elev)
963 { return std::accumulate(elev.mAzs.cbegin(), elev.mAzs.cend(), levels, measure_azi); };
964 auto measure_field = [measure_elev](const LevelPair levels, const HrirFdT &field)
965 { return std::accumulate(field.mEvs.cbegin(), field.mEvs.cend(), levels, measure_elev); };
967 const auto maxlev = std::accumulate(hData->mFds.begin(), hData->mFds.end(),
968 LevelPair{0.0, 0.0}, measure_field);
970 /* Normalize using the maximum RMS of the HRIRs. The RMS measure for the
971 * non-filtered signal is of an impulse with equal length (to the filter):
973 * rms_impulse = sqrt(sum([ 1^2, 0^2, 0^2, ... ]) / n)
974 * = sqrt(1 / n)
976 * This helps keep a more consistent volume between the non-filtered signal
977 * and various data sets.
979 double factor{std::sqrt(1.0 / irSize) / maxlev.rms};
981 /* Also ensure the samples themselves won't clip. */
982 factor = std::min(factor, 0.99/maxlev.amp);
984 /* Now scale all IRs by the given factor. */
985 auto proc_channel = [irSize,factor](al::span<double> ir)
987 ir = ir.first(irSize);
988 std::transform(ir.cbegin(), ir.cend(), ir.begin(),
989 [factor](double s) { return s * factor; });
991 auto proc_azi = [channels,proc_channel](HrirAzT &azi)
992 { std::for_each(azi.mIrs.begin(), azi.mIrs.begin()+channels, proc_channel); };
993 auto proc_elev = [proc_azi](HrirEvT &elev)
994 { std::for_each(elev.mAzs.begin(), elev.mAzs.end(), proc_azi); };
995 auto proc1_field = [proc_elev](HrirFdT &field)
996 { std::for_each(field.mEvs.begin(), field.mEvs.end(), proc_elev); };
998 std::for_each(hData->mFds.begin(), hData->mFds.end(), proc1_field);
1001 // Calculate the left-ear time delay using a spherical head model.
1002 double CalcLTD(const double ev, const double az, const double rad, const double dist)
1004 double azp, dlp, l, al;
1006 azp = std::asin(std::cos(ev) * std::sin(az));
1007 dlp = std::sqrt((dist*dist) + (rad*rad) + (2.0*dist*rad*sin(azp)));
1008 l = std::sqrt((dist*dist) - (rad*rad));
1009 al = (0.5 * al::numbers::pi) + azp;
1010 if(dlp > l)
1011 dlp = l + (rad * (al - std::acos(rad / dist)));
1012 return dlp / 343.3;
1015 // Calculate the effective head-related time delays for each minimum-phase
1016 // HRIR. This is done per-field since distance delay is ignored.
1017 void CalculateHrtds(const HeadModelT model, const double radius, HrirDataT *hData)
1019 uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1;
1020 double customRatio{radius / hData->mRadius};
1021 uint ti;
1023 if(model == HM_Sphere)
1025 for(auto &field : hData->mFds)
1027 for(auto &elev : field.mEvs)
1029 for(auto &azd : elev.mAzs)
1031 for(ti = 0;ti < channels;ti++)
1032 azd.mDelays[ti] = CalcLTD(elev.mElevation, azd.mAzimuth, radius, field.mDistance);
1037 else if(customRatio != 1.0)
1039 for(auto &field : hData->mFds)
1041 for(auto &elev : field.mEvs)
1043 for(auto &azd : elev.mAzs)
1045 for(ti = 0;ti < channels;ti++)
1046 azd.mDelays[ti] *= customRatio;
1052 double maxHrtd{0.0};
1053 for(auto &field : hData->mFds)
1055 double minHrtd{std::numeric_limits<double>::infinity()};
1056 for(auto &elev : field.mEvs)
1058 for(auto &azd : elev.mAzs)
1060 for(ti = 0;ti < channels;ti++)
1061 minHrtd = std::min(azd.mDelays[ti], minHrtd);
1065 for(auto &elev : field.mEvs)
1067 for(auto &azd : elev.mAzs)
1069 for(ti = 0;ti < channels;ti++)
1071 azd.mDelays[ti] = (azd.mDelays[ti]-minHrtd) * hData->mIrRate;
1072 maxHrtd = std::max(maxHrtd, azd.mDelays[ti]);
1077 if(maxHrtd > MaxHrtd)
1079 printf(" Scaling for max delay of %f samples to %f\n...\n", maxHrtd, MaxHrtd);
1080 const double scale{MaxHrtd / maxHrtd};
1081 for(auto &field : hData->mFds)
1083 for(auto &elev : field.mEvs)
1085 for(auto &azd : elev.mAzs)
1087 for(ti = 0;ti < channels;ti++)
1088 azd.mDelays[ti] *= scale;
1095 } // namespace
1097 // Allocate and configure dynamic HRIR structures.
1098 bool PrepareHrirData(const al::span<const double> distances,
1099 const al::span<const uint,MAX_FD_COUNT> evCounts,
1100 const al::span<const std::array<uint,MAX_EV_COUNT>,MAX_FD_COUNT> azCounts, HrirDataT *hData)
1102 uint evTotal{0}, azTotal{0};
1104 for(size_t fi{0};fi < distances.size();++fi)
1106 evTotal += evCounts[fi];
1107 for(size_t ei{0};ei < evCounts[fi];++ei)
1108 azTotal += azCounts[fi][ei];
1110 if(!evTotal || !azTotal)
1111 return false;
1113 hData->mEvsBase.resize(evTotal);
1114 hData->mAzsBase.resize(azTotal);
1115 hData->mFds.resize(distances.size());
1116 hData->mIrCount = azTotal;
1117 evTotal = 0;
1118 azTotal = 0;
1119 for(size_t fi{0};fi < distances.size();++fi)
1121 hData->mFds[fi].mDistance = distances[fi];
1122 hData->mFds[fi].mEvStart = 0;
1123 hData->mFds[fi].mEvs = al::span{hData->mEvsBase}.subspan(evTotal, evCounts[fi]);
1124 evTotal += evCounts[fi];
1125 for(uint ei{0};ei < evCounts[fi];++ei)
1127 uint azCount = azCounts[fi][ei];
1129 hData->mFds[fi].mEvs[ei].mElevation = -al::numbers::pi / 2.0 + al::numbers::pi * ei /
1130 (evCounts[fi] - 1);
1131 hData->mFds[fi].mEvs[ei].mAzs = al::span{hData->mAzsBase}.subspan(azTotal, azCount);
1132 for(uint ai{0};ai < azCount;ai++)
1134 hData->mFds[fi].mEvs[ei].mAzs[ai].mAzimuth = 2.0 * al::numbers::pi * ai / azCount;
1135 hData->mFds[fi].mEvs[ei].mAzs[ai].mIndex = azTotal + ai;
1136 hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[0] = 0.0;
1137 hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[1] = 0.0;
1138 hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[0] = {};
1139 hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[1] = {};
1141 azTotal += azCount;
1144 return true;
1148 namespace {
1150 /* Parse the data set definition and process the source data, storing the
1151 * resulting data set as desired. If the input name is NULL it will read
1152 * from standard input.
1154 bool ProcessDefinition(std::string_view inName, const uint outRate, const ChannelModeT chanMode,
1155 const bool farfield, const uint numThreads, const uint fftSize, const bool equalize,
1156 const bool surface, const double limit, const uint truncSize, const HeadModelT model,
1157 const double radius, const std::string_view outName)
1159 HrirDataT hData;
1161 printf("Using %u thread%s.\n", numThreads, (numThreads==1)?"":"s");
1162 if(inName.empty() || inName == "-"sv)
1164 inName = "stdin"sv;
1165 printf("Reading HRIR definition from %.*s...\n", al::sizei(inName), inName.data());
1166 if(!LoadDefInput(std::cin, {}, inName, fftSize, truncSize, outRate, chanMode, &hData))
1167 return false;
1169 else
1171 auto input = std::make_unique<std::ifstream>(std::filesystem::u8path(inName));
1172 if(!input->is_open())
1174 fprintf(stderr, "Error: Could not open input file '%.*s'\n", al::sizei(inName),
1175 inName.data());
1176 return false;
1179 std::array<char,4> startbytes{};
1180 input->read(startbytes.data(), startbytes.size());
1181 if(input->gcount() != startbytes.size() || !input->good())
1183 fprintf(stderr, "Error: Could not read input file '%.*s'\n", al::sizei(inName),
1184 inName.data());
1185 return false;
1188 if(startbytes[0] == '\x89' && startbytes[1] == 'H' && startbytes[2] == 'D'
1189 && startbytes[3] == 'F')
1191 input = nullptr;
1192 printf("Reading HRTF data from %.*s...\n", al::sizei(inName), inName.data());
1193 if(!LoadSofaFile(inName, numThreads, fftSize, truncSize, outRate, chanMode, &hData))
1194 return false;
1196 else
1198 printf("Reading HRIR definition from %.*s...\n", al::sizei(inName), inName.data());
1199 if(!LoadDefInput(*input, startbytes, inName, fftSize, truncSize, outRate, chanMode,
1200 &hData))
1201 return false;
1205 if(equalize)
1207 uint c{(hData.mChannelType == CT_STEREO) ? 2u : 1u};
1208 uint m{hData.mFftSize/2u + 1u};
1209 auto dfa = std::vector<double>(size_t{c} * m);
1211 if(hData.mFds.size() > 1)
1213 fputs("Balancing field magnitudes...\n", stdout);
1214 BalanceFieldMagnitudes(&hData, c, m);
1216 fputs("Calculating diffuse-field average...\n", stdout);
1217 CalculateDiffuseFieldAverage(&hData, c, m, surface, limit, dfa);
1218 fputs("Performing diffuse-field equalization...\n", stdout);
1219 DiffuseFieldEqualize(c, m, dfa, &hData);
1221 if(hData.mFds.size() > 1)
1223 printf("Sorting %zu fields...\n", hData.mFds.size());
1224 std::sort(hData.mFds.begin(), hData.mFds.end(),
1225 [](const HrirFdT &lhs, const HrirFdT &rhs) noexcept
1226 { return lhs.mDistance < rhs.mDistance; });
1227 if(farfield)
1229 printf("Clearing %zu near field%s...\n", hData.mFds.size()-1,
1230 (hData.mFds.size()-1 != 1) ? "s" : "");
1231 hData.mFds.erase(hData.mFds.cbegin(), hData.mFds.cend()-1);
1234 fputs("Synthesizing missing elevations...\n", stdout);
1235 if(model == HM_Dataset)
1236 SynthesizeOnsets(&hData);
1237 SynthesizeHrirs(&hData);
1238 fputs("Performing minimum phase reconstruction...\n", stdout);
1239 ReconstructHrirs(&hData, numThreads);
1240 fputs("Truncating minimum-phase HRIRs...\n", stdout);
1241 hData.mIrPoints = truncSize;
1242 fputs("Normalizing final HRIRs...\n", stdout);
1243 NormalizeHrirs(&hData);
1244 fputs("Calculating impulse delays...\n", stdout);
1245 CalculateHrtds(model, (radius > DefaultCustomRadius) ? radius : hData.mRadius, &hData);
1247 const auto rateStr = std::to_string(hData.mIrRate);
1248 const auto expName = StrSubst(outName, "%r"sv, rateStr);
1249 printf("Creating MHR data set %s...\n", expName.c_str());
1250 return StoreMhr(&hData, expName);
1253 void PrintHelp(const std::string_view argv0, FILE *ofile)
1255 fprintf(ofile, "Usage: %.*s [<option>...]\n\n", al::sizei(argv0), argv0.data());
1256 fprintf(ofile, "Options:\n");
1257 fprintf(ofile, " -r <rate> Change the data set sample rate to the specified value and\n");
1258 fprintf(ofile, " resample the HRIRs accordingly.\n");
1259 fprintf(ofile, " -m Change the data set to mono, mirroring the left ear for the\n");
1260 fprintf(ofile, " right ear.\n");
1261 fprintf(ofile, " -a Change the data set to single field, using the farthest field.\n");
1262 fprintf(ofile, " -j <threads> Number of threads used to process HRIRs (default: 2).\n");
1263 fprintf(ofile, " -f <points> Override the FFT window size (default: %u).\n", DefaultFftSize);
1264 fprintf(ofile, " -e {on|off} Toggle diffuse-field equalization (default: %s).\n", (DefaultEqualize ? "on" : "off"));
1265 fprintf(ofile, " -s {on|off} Toggle surface-weighted diffuse-field average (default: %s).\n", (DefaultSurface ? "on" : "off"));
1266 fprintf(ofile, " -l {<dB>|none} Specify a limit to the magnitude range of the diffuse-field\n");
1267 fprintf(ofile, " average (default: %.2f).\n", DefaultLimit);
1268 fprintf(ofile, " -w <points> Specify the size of the truncation window that's applied\n");
1269 fprintf(ofile, " after minimum-phase reconstruction (default: %u).\n", DefaultTruncSize);
1270 fprintf(ofile, " -d {dataset| Specify the model used for calculating the head-delay timing\n");
1271 fprintf(ofile, " sphere} values (default: %s).\n", ((HM_Default == HM_Dataset) ? "dataset" : "sphere"));
1272 fprintf(ofile, " -c <radius> Use a customized head radius measured to-ear in meters.\n");
1273 fprintf(ofile, " -i <filename> Specify an HRIR definition file to use (defaults to stdin).\n");
1274 fprintf(ofile, " -o <filename> Specify an output file. Use of '%%r' will be substituted with\n");
1275 fprintf(ofile, " the data set sample rate.\n");
1278 // Standard command line dispatch.
1279 int main(al::span<std::string_view> args)
1281 if(args.size() < 2)
1283 fputs("HRTF Processing and Composition Utility\n\n", stdout);
1284 PrintHelp(args[0], stdout);
1285 exit(EXIT_SUCCESS);
1288 std::string_view outName{"./oalsoft_hrtf_%r.mhr"sv};
1289 uint outRate{0};
1290 ChannelModeT chanMode{CM_AllowStereo};
1291 uint fftSize{DefaultFftSize};
1292 bool equalize{DefaultEqualize};
1293 bool surface{DefaultSurface};
1294 double limit{DefaultLimit};
1295 uint numThreads{2};
1296 uint truncSize{DefaultTruncSize};
1297 HeadModelT model{HM_Default};
1298 double radius{DefaultCustomRadius};
1299 bool farfield{false};
1300 std::string_view inName;
1302 const std::string_view optlist{"r:maj:f:e:s:l:w:d:c:e:i:o:h"sv};
1303 const auto arg0 = args[0];
1304 args = args.subspan(1);
1305 std::string_view optarg;
1306 size_t argplace{0};
1308 auto getarg = [&args,&argplace,&optarg,optlist]
1310 while(!args.empty() && argplace >= args[0].size())
1312 argplace = 0;
1313 args = args.subspan(1);
1315 if(args.empty())
1316 return 0;
1318 if(argplace == 0)
1320 if(args[0] == "--"sv)
1321 return 0;
1323 if(args[0][0] != '-' || args[0].size() == 1)
1325 fprintf(stderr, "Invalid argument: %.*s\n", al::sizei(args[0]), args[0].data());
1326 return -1;
1328 ++argplace;
1331 const char nextopt{args[0][argplace]};
1332 const auto listidx = optlist.find(nextopt);
1333 if(listidx >= optlist.size())
1335 fprintf(stderr, "Unknown argument: -%c\n", nextopt);
1336 return -1;
1338 const bool needsarg{listidx+1 < optlist.size() && optlist[listidx+1] == ':'};
1339 if(needsarg && (argplace+1 < args[0].size() || args.size() < 2))
1341 fprintf(stderr, "Missing parameter for argument: -%c\n", nextopt);
1342 return -1;
1344 if(++argplace == args[0].size())
1346 if(needsarg)
1347 optarg = args[1];
1348 argplace = 0;
1349 args = args.subspan(1u + needsarg);
1352 return int{nextopt};
1355 while(auto opt = getarg())
1357 std::size_t endpos{};
1358 switch(opt)
1360 case 'r':
1361 outRate = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1362 if(endpos != optarg.size() || outRate < MIN_RATE || outRate > MAX_RATE)
1364 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1365 al::sizei(optarg), optarg.data(), opt, MIN_RATE, MAX_RATE);
1366 exit(EXIT_FAILURE);
1368 break;
1370 case 'm':
1371 chanMode = CM_ForceMono;
1372 break;
1374 case 'a':
1375 farfield = true;
1376 break;
1378 case 'j':
1379 numThreads = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1380 if(endpos != optarg.size() || numThreads > 64)
1382 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1383 al::sizei(optarg), optarg.data(), opt, 0, 64);
1384 exit(EXIT_FAILURE);
1386 if(numThreads == 0)
1387 numThreads = std::thread::hardware_concurrency();
1388 break;
1390 case 'f':
1391 fftSize = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1392 if(endpos != optarg.size() || (fftSize&(fftSize-1)) || fftSize < MinFftSize
1393 || fftSize > MaxFftSize)
1395 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected a power-of-two between %u to %u.\n",
1396 al::sizei(optarg), optarg.data(), opt, MinFftSize, MaxFftSize);
1397 exit(EXIT_FAILURE);
1399 break;
1401 case 'e':
1402 if(optarg == "on"sv)
1403 equalize = true;
1404 else if(optarg == "off"sv)
1405 equalize = false;
1406 else
1408 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected on or off.\n",
1409 al::sizei(optarg), optarg.data(), opt);
1410 exit(EXIT_FAILURE);
1412 break;
1414 case 's':
1415 if(optarg == "on"sv)
1416 surface = true;
1417 else if(optarg == "off"sv)
1418 surface = false;
1419 else
1421 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected on or off.\n",
1422 al::sizei(optarg), optarg.data(), opt);
1423 exit(EXIT_FAILURE);
1425 break;
1427 case 'l':
1428 if(optarg == "none"sv)
1429 limit = 0.0;
1430 else
1432 limit = std::stod(std::string{optarg}, &endpos);
1433 if(endpos != optarg.size() || limit < MinLimit || limit > MaxLimit)
1435 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %.0f to %.0f.\n",
1436 al::sizei(optarg), optarg.data(), opt, MinLimit, MaxLimit);
1437 exit(EXIT_FAILURE);
1440 break;
1442 case 'w':
1443 truncSize = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1444 if(endpos != optarg.size() || truncSize < MinTruncSize || truncSize > MaxTruncSize)
1446 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1447 al::sizei(optarg), optarg.data(), opt, MinTruncSize, MaxTruncSize);
1448 exit(EXIT_FAILURE);
1450 break;
1452 case 'd':
1453 if(optarg == "dataset"sv)
1454 model = HM_Dataset;
1455 else if(optarg == "sphere"sv)
1456 model = HM_Sphere;
1457 else
1459 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected dataset or sphere.\n",
1460 al::sizei(optarg), optarg.data(), opt);
1461 exit(EXIT_FAILURE);
1463 break;
1465 case 'c':
1466 radius = std::stod(std::string{optarg}, &endpos);
1467 if(endpos != optarg.size() || radius < MinCustomRadius || radius > MaxCustomRadius)
1469 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %.2f to %.2f.\n",
1470 al::sizei(optarg), optarg.data(), opt, MinCustomRadius, MaxCustomRadius);
1471 exit(EXIT_FAILURE);
1473 break;
1475 case 'i':
1476 inName = optarg;
1477 break;
1479 case 'o':
1480 outName = optarg;
1481 break;
1483 case 'h':
1484 PrintHelp(arg0, stdout);
1485 exit(EXIT_SUCCESS);
1487 default: /* '?' */
1488 PrintHelp(arg0, stderr);
1489 exit(EXIT_FAILURE);
1493 const int ret{ProcessDefinition(inName, outRate, chanMode, farfield, numThreads, fftSize,
1494 equalize, surface, limit, truncSize, model, radius, outName)};
1495 if(!ret) return -1;
1496 fputs("Operation completed.\n", stdout);
1498 return EXIT_SUCCESS;
1501 } /* namespace */
1503 int main(int argc, char **argv)
1505 assert(argc >= 0);
1506 auto args = std::vector<std::string_view>(static_cast<unsigned int>(argc));
1507 std::copy_n(argv, args.size(), args.begin());
1508 return main(al::span{args});