Enable proper full C++ exception handling on MSVC
[openal-soft.git] / utils / makemhr / makemhr.cpp
blob508fd996a8eb395c43acc44784da635c9421989f
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 fprintf(stdout, " 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 fprintf(stdout, "Using %u thread%s.\n", numThreads, (numThreads==1)?"":"s");
1162 if(inName.empty() || inName == "-"sv)
1164 inName = "stdin"sv;
1165 fprintf(stdout, "Reading HRIR definition from %.*s...\n", al::sizei(inName),
1166 inName.data());
1167 if(!LoadDefInput(std::cin, {}, inName, fftSize, truncSize, outRate, chanMode, &hData))
1168 return false;
1170 else
1172 auto input = std::make_unique<std::ifstream>(std::filesystem::u8path(inName));
1173 if(!input->is_open())
1175 fprintf(stderr, "Error: Could not open input file '%.*s'\n", al::sizei(inName),
1176 inName.data());
1177 return false;
1180 std::array<char,4> startbytes{};
1181 input->read(startbytes.data(), startbytes.size());
1182 if(input->gcount() != startbytes.size() || !input->good())
1184 fprintf(stderr, "Error: Could not read input file '%.*s'\n", al::sizei(inName),
1185 inName.data());
1186 return false;
1189 if(startbytes[0] == '\x89' && startbytes[1] == 'H' && startbytes[2] == 'D'
1190 && startbytes[3] == 'F')
1192 input = nullptr;
1193 fprintf(stdout, "Reading HRTF data from %.*s...\n", al::sizei(inName),
1194 inName.data());
1195 if(!LoadSofaFile(inName, numThreads, fftSize, truncSize, outRate, chanMode, &hData))
1196 return false;
1198 else
1200 fprintf(stdout, "Reading HRIR definition from %.*s...\n", al::sizei(inName),
1201 inName.data());
1202 if(!LoadDefInput(*input, startbytes, inName, fftSize, truncSize, outRate, chanMode,
1203 &hData))
1204 return false;
1208 if(equalize)
1210 uint c{(hData.mChannelType == CT_STEREO) ? 2u : 1u};
1211 uint m{hData.mFftSize/2u + 1u};
1212 auto dfa = std::vector<double>(size_t{c} * m);
1214 if(hData.mFds.size() > 1)
1216 fprintf(stdout, "Balancing field magnitudes...\n");
1217 BalanceFieldMagnitudes(&hData, c, m);
1219 fprintf(stdout, "Calculating diffuse-field average...\n");
1220 CalculateDiffuseFieldAverage(&hData, c, m, surface, limit, dfa);
1221 fprintf(stdout, "Performing diffuse-field equalization...\n");
1222 DiffuseFieldEqualize(c, m, dfa, &hData);
1224 if(hData.mFds.size() > 1)
1226 fprintf(stdout, "Sorting %zu fields...\n", hData.mFds.size());
1227 std::sort(hData.mFds.begin(), hData.mFds.end(),
1228 [](const HrirFdT &lhs, const HrirFdT &rhs) noexcept
1229 { return lhs.mDistance < rhs.mDistance; });
1230 if(farfield)
1232 fprintf(stdout, "Clearing %zu near field%s...\n", hData.mFds.size()-1,
1233 (hData.mFds.size()-1 != 1) ? "s" : "");
1234 hData.mFds.erase(hData.mFds.cbegin(), hData.mFds.cend()-1);
1237 fprintf(stdout, "Synthesizing missing elevations...\n");
1238 if(model == HM_Dataset)
1239 SynthesizeOnsets(&hData);
1240 SynthesizeHrirs(&hData);
1241 fprintf(stdout, "Performing minimum phase reconstruction...\n");
1242 ReconstructHrirs(&hData, numThreads);
1243 fprintf(stdout, "Truncating minimum-phase HRIRs...\n");
1244 hData.mIrPoints = truncSize;
1245 fprintf(stdout, "Normalizing final HRIRs...\n");
1246 NormalizeHrirs(&hData);
1247 fprintf(stdout, "Calculating impulse delays...\n");
1248 CalculateHrtds(model, (radius > DefaultCustomRadius) ? radius : hData.mRadius, &hData);
1250 const auto rateStr = std::to_string(hData.mIrRate);
1251 const auto expName = StrSubst(outName, "%r"sv, rateStr);
1252 fprintf(stdout, "Creating MHR data set %s...\n", expName.c_str());
1253 return StoreMhr(&hData, expName);
1256 void PrintHelp(const std::string_view argv0, FILE *ofile)
1258 fprintf(ofile, "Usage: %.*s [<option>...]\n\n", al::sizei(argv0), argv0.data());
1259 fprintf(ofile, "Options:\n");
1260 fprintf(ofile, " -r <rate> Change the data set sample rate to the specified value and\n");
1261 fprintf(ofile, " resample the HRIRs accordingly.\n");
1262 fprintf(ofile, " -m Change the data set to mono, mirroring the left ear for the\n");
1263 fprintf(ofile, " right ear.\n");
1264 fprintf(ofile, " -a Change the data set to single field, using the farthest field.\n");
1265 fprintf(ofile, " -j <threads> Number of threads used to process HRIRs (default: 2).\n");
1266 fprintf(ofile, " -f <points> Override the FFT window size (default: %u).\n", DefaultFftSize);
1267 fprintf(ofile, " -e {on|off} Toggle diffuse-field equalization (default: %s).\n", (DefaultEqualize ? "on" : "off"));
1268 fprintf(ofile, " -s {on|off} Toggle surface-weighted diffuse-field average (default: %s).\n", (DefaultSurface ? "on" : "off"));
1269 fprintf(ofile, " -l {<dB>|none} Specify a limit to the magnitude range of the diffuse-field\n");
1270 fprintf(ofile, " average (default: %.2f).\n", DefaultLimit);
1271 fprintf(ofile, " -w <points> Specify the size of the truncation window that's applied\n");
1272 fprintf(ofile, " after minimum-phase reconstruction (default: %u).\n", DefaultTruncSize);
1273 fprintf(ofile, " -d {dataset| Specify the model used for calculating the head-delay timing\n");
1274 fprintf(ofile, " sphere} values (default: %s).\n", ((HM_Default == HM_Dataset) ? "dataset" : "sphere"));
1275 fprintf(ofile, " -c <radius> Use a customized head radius measured to-ear in meters.\n");
1276 fprintf(ofile, " -i <filename> Specify an HRIR definition file to use (defaults to stdin).\n");
1277 fprintf(ofile, " -o <filename> Specify an output file. Use of '%%r' will be substituted with\n");
1278 fprintf(ofile, " the data set sample rate.\n");
1281 // Standard command line dispatch.
1282 int main(al::span<std::string_view> args)
1284 if(args.size() < 2)
1286 fprintf(stdout, "HRTF Processing and Composition Utility\n\n");
1287 PrintHelp(args[0], stdout);
1288 exit(EXIT_SUCCESS);
1291 std::string_view outName{"./oalsoft_hrtf_%r.mhr"sv};
1292 uint outRate{0};
1293 ChannelModeT chanMode{CM_AllowStereo};
1294 uint fftSize{DefaultFftSize};
1295 bool equalize{DefaultEqualize};
1296 bool surface{DefaultSurface};
1297 double limit{DefaultLimit};
1298 uint numThreads{2};
1299 uint truncSize{DefaultTruncSize};
1300 HeadModelT model{HM_Default};
1301 double radius{DefaultCustomRadius};
1302 bool farfield{false};
1303 std::string_view inName;
1305 const std::string_view optlist{"r:maj:f:e:s:l:w:d:c:e:i:o:h"sv};
1306 const auto arg0 = args[0];
1307 args = args.subspan(1);
1308 std::string_view optarg;
1309 size_t argplace{0};
1311 auto getarg = [&args,&argplace,&optarg,optlist]
1313 while(!args.empty() && argplace >= args[0].size())
1315 argplace = 0;
1316 args = args.subspan(1);
1318 if(args.empty())
1319 return 0;
1321 if(argplace == 0)
1323 if(args[0] == "--"sv)
1324 return 0;
1326 if(args[0][0] != '-' || args[0].size() == 1)
1328 fprintf(stderr, "Invalid argument: %.*s\n", al::sizei(args[0]), args[0].data());
1329 return -1;
1331 ++argplace;
1334 const char nextopt{args[0][argplace]};
1335 const auto listidx = optlist.find(nextopt);
1336 if(listidx >= optlist.size())
1338 fprintf(stderr, "Unknown argument: -%c\n", nextopt);
1339 return -1;
1341 const bool needsarg{listidx+1 < optlist.size() && optlist[listidx+1] == ':'};
1342 if(needsarg && (argplace+1 < args[0].size() || args.size() < 2))
1344 fprintf(stderr, "Missing parameter for argument: -%c\n", nextopt);
1345 return -1;
1347 if(++argplace == args[0].size())
1349 if(needsarg)
1350 optarg = args[1];
1351 argplace = 0;
1352 args = args.subspan(1u + needsarg);
1355 return int{nextopt};
1358 while(auto opt = getarg())
1360 std::size_t endpos{};
1361 switch(opt)
1363 case 'r':
1364 outRate = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1365 if(endpos != optarg.size() || outRate < MIN_RATE || outRate > MAX_RATE)
1367 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1368 al::sizei(optarg), optarg.data(), opt, MIN_RATE, MAX_RATE);
1369 exit(EXIT_FAILURE);
1371 break;
1373 case 'm':
1374 chanMode = CM_ForceMono;
1375 break;
1377 case 'a':
1378 farfield = true;
1379 break;
1381 case 'j':
1382 numThreads = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1383 if(endpos != optarg.size() || numThreads > 64)
1385 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1386 al::sizei(optarg), optarg.data(), opt, 0, 64);
1387 exit(EXIT_FAILURE);
1389 if(numThreads == 0)
1390 numThreads = std::thread::hardware_concurrency();
1391 break;
1393 case 'f':
1394 fftSize = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1395 if(endpos != optarg.size() || (fftSize&(fftSize-1)) || fftSize < MinFftSize
1396 || fftSize > MaxFftSize)
1398 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected a power-of-two between %u to %u.\n",
1399 al::sizei(optarg), optarg.data(), opt, MinFftSize, MaxFftSize);
1400 exit(EXIT_FAILURE);
1402 break;
1404 case 'e':
1405 if(optarg == "on"sv)
1406 equalize = true;
1407 else if(optarg == "off"sv)
1408 equalize = false;
1409 else
1411 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected on or off.\n",
1412 al::sizei(optarg), optarg.data(), opt);
1413 exit(EXIT_FAILURE);
1415 break;
1417 case 's':
1418 if(optarg == "on"sv)
1419 surface = true;
1420 else if(optarg == "off"sv)
1421 surface = false;
1422 else
1424 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected on or off.\n",
1425 al::sizei(optarg), optarg.data(), opt);
1426 exit(EXIT_FAILURE);
1428 break;
1430 case 'l':
1431 if(optarg == "none"sv)
1432 limit = 0.0;
1433 else
1435 limit = std::stod(std::string{optarg}, &endpos);
1436 if(endpos != optarg.size() || limit < MinLimit || limit > MaxLimit)
1438 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %.0f to %.0f.\n",
1439 al::sizei(optarg), optarg.data(), opt, MinLimit, MaxLimit);
1440 exit(EXIT_FAILURE);
1443 break;
1445 case 'w':
1446 truncSize = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1447 if(endpos != optarg.size() || truncSize < MinTruncSize || truncSize > MaxTruncSize)
1449 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1450 al::sizei(optarg), optarg.data(), opt, MinTruncSize, MaxTruncSize);
1451 exit(EXIT_FAILURE);
1453 break;
1455 case 'd':
1456 if(optarg == "dataset"sv)
1457 model = HM_Dataset;
1458 else if(optarg == "sphere"sv)
1459 model = HM_Sphere;
1460 else
1462 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected dataset or sphere.\n",
1463 al::sizei(optarg), optarg.data(), opt);
1464 exit(EXIT_FAILURE);
1466 break;
1468 case 'c':
1469 radius = std::stod(std::string{optarg}, &endpos);
1470 if(endpos != optarg.size() || radius < MinCustomRadius || radius > MaxCustomRadius)
1472 fprintf(stderr, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %.2f to %.2f.\n",
1473 al::sizei(optarg), optarg.data(), opt, MinCustomRadius, MaxCustomRadius);
1474 exit(EXIT_FAILURE);
1476 break;
1478 case 'i':
1479 inName = optarg;
1480 break;
1482 case 'o':
1483 outName = optarg;
1484 break;
1486 case 'h':
1487 PrintHelp(arg0, stdout);
1488 exit(EXIT_SUCCESS);
1490 default: /* '?' */
1491 PrintHelp(arg0, stderr);
1492 exit(EXIT_FAILURE);
1496 const int ret{ProcessDefinition(inName, outRate, chanMode, farfield, numThreads, fftSize,
1497 equalize, surface, limit, truncSize, model, radius, outName)};
1498 if(!ret) return -1;
1499 fprintf(stdout, "Operation completed.\n");
1501 return EXIT_SUCCESS;
1504 } /* namespace */
1506 int main(int argc, char **argv)
1508 assert(argc >= 0);
1509 auto args = std::vector<std::string_view>(static_cast<unsigned int>(argc));
1510 std::copy_n(argv, args.size(), args.begin());
1511 return main(al::span{args});