Remove some unnecessary casts
[openal-soft.git] / utils / makemhr / makemhr.cpp
blob4d9c15ff34c4cf8195714af411aee7aa5dd0048b
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 <vector>
87 #include "alcomplex.h"
88 #include "alnumbers.h"
89 #include "alnumeric.h"
90 #include "alspan.h"
91 #include "alstring.h"
92 #include "fmt/core.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 fmt::println(stderr, "\nError: Bad write to file '{}'.", filename);
287 return 0;
289 return 1;
292 // Write a binary value of the given byte order and byte size to a file,
293 // loading it from a 32-bit unsigned integer.
294 auto WriteBin4(const uint bytes, const uint32_t in, std::ostream &ostream,
295 const std::string_view filename) -> int
297 std::array<char,4> out{};
298 for(uint i{0};i < bytes;i++)
299 out[i] = static_cast<char>((in>>(i*8)) & 0x000000FF);
301 if(!ostream.write(out.data(), std::streamsize(bytes)) || ostream.bad())
303 fmt::println(stderr, "\nError: Bad write to file '{}'.", filename);
304 return 0;
306 return 1;
309 // Store the OpenAL Soft HRTF data set.
310 auto StoreMhr(const HrirDataT *hData, const std::string_view filename) -> bool
312 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
313 const uint n{hData->mIrPoints};
314 uint dither_seed{22222};
316 auto ostream = std::ofstream{std::filesystem::u8path(filename), std::ios::binary};
317 if(!ostream.is_open())
319 fmt::println(stderr, "\nError: Could not open MHR file '{}'.", filename);
320 return false;
322 if(!WriteAscii(GetMHRMarker(), ostream, filename))
323 return false;
324 if(!WriteBin4(4, hData->mIrRate, ostream, filename))
325 return false;
326 if(!WriteBin4(1, static_cast<uint32_t>(hData->mChannelType), ostream, filename))
327 return false;
328 if(!WriteBin4(1, hData->mIrPoints, ostream, filename))
329 return false;
330 if(!WriteBin4(1, static_cast<uint>(hData->mFds.size()), ostream, filename))
331 return false;
332 for(size_t fi{hData->mFds.size()-1};fi < hData->mFds.size();--fi)
334 auto fdist = static_cast<uint32_t>(std::round(1000.0 * hData->mFds[fi].mDistance));
335 if(!WriteBin4(2, fdist, ostream, filename))
336 return false;
337 if(!WriteBin4(1, static_cast<uint32_t>(hData->mFds[fi].mEvs.size()), ostream, filename))
338 return false;
339 for(size_t ei{0};ei < hData->mFds[fi].mEvs.size();++ei)
341 const auto &elev = hData->mFds[fi].mEvs[ei];
342 if(!WriteBin4(1, static_cast<uint32_t>(elev.mAzs.size()), ostream, filename))
343 return false;
347 for(size_t fi{hData->mFds.size()-1};fi < hData->mFds.size();--fi)
349 static constexpr double scale{8388607.0};
350 static constexpr uint bps{3u};
352 for(const auto &evd : hData->mFds[fi].mEvs)
354 for(const auto &azd : evd.mAzs)
356 std::array<double,MaxTruncSize*2_uz> out{};
358 TpdfDither(out, azd.mIrs[0].first(n), scale, 0, channels, &dither_seed);
359 if(hData->mChannelType == CT_STEREO)
360 TpdfDither(out, azd.mIrs[1].first(n), scale, 1, channels, &dither_seed);
361 const size_t numsamples{size_t{channels} * n};
362 for(size_t i{0};i < numsamples;i++)
364 const auto v = static_cast<int>(Clamp(out[i], -scale-1.0, scale));
365 if(!WriteBin4(bps, static_cast<uint32_t>(v), ostream, filename))
366 return false;
371 for(size_t fi{hData->mFds.size()-1};fi < hData->mFds.size();--fi)
373 /* Delay storage has 2 bits of extra precision. */
374 static constexpr double DelayPrecScale{4.0};
375 for(const auto &evd : hData->mFds[fi].mEvs)
377 for(const auto &azd : evd.mAzs)
379 auto v = static_cast<uint>(std::round(azd.mDelays[0]*DelayPrecScale));
380 if(!WriteBin4(1, v, ostream, filename)) return false;
381 if(hData->mChannelType == CT_STEREO)
383 v = static_cast<uint>(std::round(azd.mDelays[1]*DelayPrecScale));
384 if(!WriteBin4(1, v, ostream, filename)) return false;
389 return true;
393 /***********************
394 *** HRTF processing ***
395 ***********************/
397 /* Balances the maximum HRIR magnitudes of multi-field data sets by
398 * independently normalizing each field in relation to the overall maximum.
399 * This is done to ignore distance attenuation.
401 void BalanceFieldMagnitudes(const HrirDataT *hData, const uint channels, const uint m)
403 std::array<double,MAX_FD_COUNT> maxMags{};
404 double maxMag{0.0};
406 for(size_t fi{0};fi < hData->mFds.size();++fi)
408 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
410 for(const auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
412 for(size_t ti{0};ti < channels;++ti)
414 for(size_t i{0};i < m;++i)
415 maxMags[fi] = std::max(azd.mIrs[ti][i], maxMags[fi]);
420 maxMag = std::max(maxMags[fi], maxMag);
423 for(size_t fi{0};fi < hData->mFds.size();++fi)
425 const double magFactor{maxMag / maxMags[fi]};
427 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
429 for(const auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
431 for(size_t ti{0};ti < channels;++ti)
433 for(size_t i{0};i < m;++i)
434 azd.mIrs[ti][i] *= magFactor;
441 /* Calculate the contribution of each HRIR to the diffuse-field average based
442 * on its coverage volume. All volumes are centered at the spherical HRIR
443 * coordinates and measured by extruded solid angle.
445 void CalculateDfWeights(const HrirDataT *hData, const al::span<double> weights)
447 double sum, innerRa, outerRa, evs, ev, upperEv, lowerEv;
448 double solidAngle, solidVolume;
449 uint fi, ei;
451 sum = 0.0;
452 // The head radius acts as the limit for the inner radius.
453 innerRa = hData->mRadius;
454 for(fi = 0;fi < hData->mFds.size();fi++)
456 // Each volume ends half way between progressive field measurements.
457 if((fi + 1) < hData->mFds.size())
458 outerRa = 0.5f * (hData->mFds[fi].mDistance + hData->mFds[fi + 1].mDistance);
459 // The final volume has its limit extended to some practical value.
460 // This is done to emphasize the far-field responses in the average.
461 else
462 outerRa = 10.0f;
464 const double raPowDiff{std::pow(outerRa, 3.0) - std::pow(innerRa, 3.0)};
465 evs = al::numbers::pi / 2.0 / static_cast<double>(hData->mFds[fi].mEvs.size() - 1);
466 for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
468 const auto &elev = hData->mFds[fi].mEvs[ei];
469 // For each elevation, calculate the upper and lower limits of
470 // the patch band.
471 ev = elev.mElevation;
472 lowerEv = std::max(-al::numbers::pi / 2.0, ev - evs);
473 upperEv = std::min(al::numbers::pi / 2.0, ev + evs);
474 // Calculate the surface area of the patch band.
475 solidAngle = 2.0 * al::numbers::pi * (std::sin(upperEv) - std::sin(lowerEv));
476 // Then the volume of the extruded patch band.
477 solidVolume = solidAngle * raPowDiff / 3.0;
478 // Each weight is the volume of one extruded patch.
479 weights[(fi*MAX_EV_COUNT) + ei] = solidVolume / static_cast<double>(elev.mAzs.size());
480 // Sum the total coverage volume of the HRIRs for all fields.
481 sum += solidAngle;
484 innerRa = outerRa;
487 for(fi = 0;fi < hData->mFds.size();fi++)
489 // Normalize the weights given the total surface coverage for all
490 // fields.
491 for(ei = hData->mFds[fi].mEvStart;ei < hData->mFds[fi].mEvs.size();ei++)
492 weights[(fi * MAX_EV_COUNT) + ei] /= sum;
496 /* Calculate the diffuse-field average from the given magnitude responses of
497 * the HRIR set. Weighting can be applied to compensate for the varying
498 * coverage of each HRIR. The final average can then be limited by the
499 * specified magnitude range (in positive dB; 0.0 to skip).
501 void CalculateDiffuseFieldAverage(const HrirDataT *hData, const uint channels, const uint m,
502 const bool weighted, const double limit, const al::span<double> dfa)
504 std::vector<double> weights(hData->mFds.size() * MAX_EV_COUNT);
505 uint count;
507 if(weighted)
509 // Use coverage weighting to calculate the average.
510 CalculateDfWeights(hData, weights);
512 else
514 double weight;
516 // If coverage weighting is not used, the weights still need to be
517 // averaged by the number of existing HRIRs.
518 count = hData->mIrCount;
519 for(size_t fi{0};fi < hData->mFds.size();++fi)
521 for(size_t ei{0};ei < hData->mFds[fi].mEvStart;++ei)
522 count -= static_cast<uint>(hData->mFds[fi].mEvs[ei].mAzs.size());
524 weight = 1.0 / count;
526 for(size_t fi{0};fi < hData->mFds.size();++fi)
528 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
529 weights[(fi * MAX_EV_COUNT) + ei] = weight;
532 for(size_t ti{0};ti < channels;++ti)
534 for(size_t i{0};i < m;++i)
535 dfa[(ti * m) + i] = 0.0;
536 for(size_t fi{0};fi < hData->mFds.size();++fi)
538 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
540 for(size_t ai{0};ai < hData->mFds[fi].mEvs[ei].mAzs.size();++ai)
542 HrirAzT *azd = &hData->mFds[fi].mEvs[ei].mAzs[ai];
543 // Get the weight for this HRIR's contribution.
544 double weight = weights[(fi * MAX_EV_COUNT) + ei];
546 // Add this HRIR's weighted power average to the total.
547 for(size_t i{0};i < m;++i)
548 dfa[(ti * m) + i] += weight * azd->mIrs[ti][i] * azd->mIrs[ti][i];
552 // Finish the average calculation and keep it from being too small.
553 for(size_t i{0};i < m;++i)
554 dfa[(ti * m) + i] = std::max(sqrt(dfa[(ti * m) + i]), Epsilon);
555 // Apply a limit to the magnitude range of the diffuse-field average
556 // if desired.
557 if(limit > 0.0)
558 LimitMagnitudeResponse(hData->mFftSize, m, limit, dfa.subspan(ti * m));
562 // Perform diffuse-field equalization on the magnitude responses of the HRIR
563 // set using the given average response.
564 void DiffuseFieldEqualize(const uint channels, const uint m, const al::span<const double> dfa,
565 const HrirDataT *hData)
567 for(size_t fi{0};fi < hData->mFds.size();++fi)
569 for(size_t ei{hData->mFds[fi].mEvStart};ei < hData->mFds[fi].mEvs.size();++ei)
571 for(auto &azd : hData->mFds[fi].mEvs[ei].mAzs)
573 for(size_t ti{0};ti < channels;++ti)
575 for(size_t i{0};i < m;++i)
576 azd.mIrs[ti][i] /= dfa[(ti * m) + i];
583 /* Given field and elevation indices and an azimuth, calculate the indices of
584 * the two HRIRs that bound the coordinate along with a factor for
585 * calculating the continuous HRIR using interpolation.
587 void CalcAzIndices(const HrirFdT &field, const uint ei, const double az, uint *a0, uint *a1, double *af)
589 double f{(2.0*al::numbers::pi + az) * static_cast<double>(field.mEvs[ei].mAzs.size()) /
590 (2.0*al::numbers::pi)};
591 const uint i{static_cast<uint>(f) % static_cast<uint>(field.mEvs[ei].mAzs.size())};
593 f -= std::floor(f);
594 *a0 = i;
595 *a1 = (i + 1) % static_cast<uint>(field.mEvs[ei].mAzs.size());
596 *af = f;
599 /* Synthesize any missing onset timings at the bottom elevations of each field.
600 * This just mirrors some top elevations for the bottom, and blends the
601 * remaining elevations (not an accurate model).
603 void SynthesizeOnsets(HrirDataT *hData)
605 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
607 auto proc_field = [channels](HrirFdT &field) -> void
609 /* Get the starting elevation from the measurements, and use it as the
610 * upper elevation limit for what needs to be calculated.
612 const uint upperElevReal{field.mEvStart};
613 if(upperElevReal <= 0) return;
615 /* Get the lowest half of the missing elevations' delays by mirroring
616 * the top elevation delays. The responses are on a spherical grid
617 * centered between the ears, so these should align.
619 uint ei{};
620 if(channels > 1)
622 /* Take the polar opposite position of the desired measurement and
623 * swap the ears.
625 field.mEvs[0].mAzs[0].mDelays[0] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[1];
626 field.mEvs[0].mAzs[0].mDelays[1] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[0];
627 for(ei = 1u;ei < (upperElevReal+1)/2;++ei)
629 const uint topElev{static_cast<uint>(field.mEvs.size()-ei-1)};
631 for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
633 uint a0, a1;
634 double af;
636 /* Rotate this current azimuth by a half-circle, and lookup
637 * the mirrored elevation to find the indices for the polar
638 * opposite position (may need blending).
640 const double az{field.mEvs[ei].mAzs[ai].mAzimuth + al::numbers::pi};
641 CalcAzIndices(field, topElev, az, &a0, &a1, &af);
643 /* Blend the delays, and again, swap the ears. */
644 field.mEvs[ei].mAzs[ai].mDelays[0] = Lerp(
645 field.mEvs[topElev].mAzs[a0].mDelays[1],
646 field.mEvs[topElev].mAzs[a1].mDelays[1], af);
647 field.mEvs[ei].mAzs[ai].mDelays[1] = Lerp(
648 field.mEvs[topElev].mAzs[a0].mDelays[0],
649 field.mEvs[topElev].mAzs[a1].mDelays[0], af);
653 else
655 field.mEvs[0].mAzs[0].mDelays[0] = field.mEvs[field.mEvs.size()-1].mAzs[0].mDelays[0];
656 for(ei = 1u;ei < (upperElevReal+1)/2;++ei)
658 const uint topElev{static_cast<uint>(field.mEvs.size()-ei-1)};
660 for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
662 uint a0, a1;
663 double af;
665 /* For mono data sets, mirror the azimuth front<->back
666 * since the other ear is a mirror of what we have (e.g.
667 * the left ear's back-left is simulated with the right
668 * ear's front-right, which uses the left ear's front-left
669 * measurement).
671 double az{field.mEvs[ei].mAzs[ai].mAzimuth};
672 if(az <= al::numbers::pi) az = al::numbers::pi - az;
673 else az = (al::numbers::pi*2.0)-az + al::numbers::pi;
674 CalcAzIndices(field, topElev, az, &a0, &a1, &af);
676 field.mEvs[ei].mAzs[ai].mDelays[0] = Lerp(
677 field.mEvs[topElev].mAzs[a0].mDelays[0],
678 field.mEvs[topElev].mAzs[a1].mDelays[0], af);
682 /* Record the lowest elevation filled in with the mirrored top. */
683 const uint lowerElevFake{ei-1u};
685 /* Fill in the remaining delays using bilinear interpolation. This
686 * helps smooth the transition back to the real delays.
688 for(;ei < upperElevReal;++ei)
690 const double ef{(field.mEvs[upperElevReal].mElevation - field.mEvs[ei].mElevation) /
691 (field.mEvs[upperElevReal].mElevation - field.mEvs[lowerElevFake].mElevation)};
693 for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
695 uint a0, a1, a2, a3;
696 double af0, af1;
698 double az{field.mEvs[ei].mAzs[ai].mAzimuth};
699 CalcAzIndices(field, upperElevReal, az, &a0, &a1, &af0);
700 CalcAzIndices(field, lowerElevFake, az, &a2, &a3, &af1);
701 std::array<double,4> blend{{
702 (1.0-ef) * (1.0-af0),
703 (1.0-ef) * ( af0),
704 ( ef) * (1.0-af1),
705 ( ef) * ( af1)
708 for(uint ti{0u};ti < channels;ti++)
710 field.mEvs[ei].mAzs[ai].mDelays[ti] =
711 field.mEvs[upperElevReal].mAzs[a0].mDelays[ti]*blend[0] +
712 field.mEvs[upperElevReal].mAzs[a1].mDelays[ti]*blend[1] +
713 field.mEvs[lowerElevFake].mAzs[a2].mDelays[ti]*blend[2] +
714 field.mEvs[lowerElevFake].mAzs[a3].mDelays[ti]*blend[3];
719 std::for_each(hData->mFds.begin(), hData->mFds.end(), proc_field);
722 /* Attempt to synthesize any missing HRIRs at the bottom elevations of each
723 * field. Right now this just blends the lowest elevation HRIRs together and
724 * applies a low-pass filter to simulate body occlusion. It is a simple, if
725 * inaccurate model.
727 void SynthesizeHrirs(HrirDataT *hData)
729 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
730 auto htemp = std::vector<complex_d>(hData->mFftSize);
731 const uint m{hData->mFftSize/2u + 1u};
732 auto filter = std::vector<double>(m);
733 const double beta{3.5e-6 * hData->mIrRate};
735 auto proc_field = [channels,m,beta,&htemp,&filter](HrirFdT &field) -> void
737 const uint oi{field.mEvStart};
738 if(oi <= 0) return;
740 for(uint ti{0u};ti < channels;ti++)
742 uint a0, a1;
743 double af;
745 /* Use the lowest immediate-left response for the left ear and
746 * lowest immediate-right response for the right ear. Given no comb
747 * effects as a result of the left response reaching the right ear
748 * and vice-versa, this produces a decent phantom-center response
749 * underneath the head.
751 CalcAzIndices(field, oi, al::numbers::pi / ((ti==0) ? -2.0 : 2.0), &a0, &a1, &af);
752 for(uint i{0u};i < m;i++)
754 field.mEvs[0].mAzs[0].mIrs[ti][i] = Lerp(field.mEvs[oi].mAzs[a0].mIrs[ti][i],
755 field.mEvs[oi].mAzs[a1].mIrs[ti][i], af);
759 for(uint ei{1u};ei < field.mEvStart;ei++)
761 const double of{static_cast<double>(ei) / field.mEvStart};
762 const double b{(1.0 - of) * beta};
763 std::array<double,4> lp{};
765 /* Calculate a low-pass filter to simulate body occlusion. */
766 lp[0] = Lerp(1.0, lp[0], b);
767 lp[1] = Lerp(lp[0], lp[1], b);
768 lp[2] = Lerp(lp[1], lp[2], b);
769 lp[3] = Lerp(lp[2], lp[3], b);
770 htemp[0] = lp[3];
771 for(size_t i{1u};i < htemp.size();i++)
773 lp[0] = Lerp(0.0, lp[0], b);
774 lp[1] = Lerp(lp[0], lp[1], b);
775 lp[2] = Lerp(lp[1], lp[2], b);
776 lp[3] = Lerp(lp[2], lp[3], b);
777 htemp[i] = lp[3];
779 /* Get the filter's frequency-domain response and extract the
780 * frequency magnitudes (phase will be reconstructed later)).
782 FftForward(static_cast<uint>(htemp.size()), htemp.data());
783 std::transform(htemp.cbegin(), htemp.cbegin()+m, filter.begin(),
784 [](const complex_d c) -> double { return std::abs(c); });
786 for(uint ai{0u};ai < field.mEvs[ei].mAzs.size();ai++)
788 uint a0, a1;
789 double af;
791 CalcAzIndices(field, oi, field.mEvs[ei].mAzs[ai].mAzimuth, &a0, &a1, &af);
792 for(uint ti{0u};ti < channels;ti++)
794 for(uint i{0u};i < m;i++)
796 /* Blend the two defined HRIRs closest to this azimuth,
797 * then blend that with the synthesized -90 elevation.
799 const double s1{Lerp(field.mEvs[oi].mAzs[a0].mIrs[ti][i],
800 field.mEvs[oi].mAzs[a1].mIrs[ti][i], af)};
801 const double s{Lerp(field.mEvs[0].mAzs[0].mIrs[ti][i], s1, of)};
802 field.mEvs[ei].mAzs[ai].mIrs[ti][i] = s * filter[i];
807 const double b{beta};
808 std::array<double,4> lp{};
809 lp[0] = Lerp(1.0, lp[0], b);
810 lp[1] = Lerp(lp[0], lp[1], b);
811 lp[2] = Lerp(lp[1], lp[2], b);
812 lp[3] = Lerp(lp[2], lp[3], b);
813 htemp[0] = lp[3];
814 for(size_t i{1u};i < htemp.size();i++)
816 lp[0] = Lerp(0.0, lp[0], b);
817 lp[1] = Lerp(lp[0], lp[1], b);
818 lp[2] = Lerp(lp[1], lp[2], b);
819 lp[3] = Lerp(lp[2], lp[3], b);
820 htemp[i] = lp[3];
822 FftForward(static_cast<uint>(htemp.size()), htemp.data());
823 std::transform(htemp.cbegin(), htemp.cbegin()+m, filter.begin(),
824 [](const complex_d c) -> double { return std::abs(c); });
826 for(uint ti{0u};ti < channels;ti++)
828 for(uint i{0u};i < m;i++)
829 field.mEvs[0].mAzs[0].mIrs[ti][i] *= filter[i];
832 std::for_each(hData->mFds.begin(), hData->mFds.end(), proc_field);
835 // The following routines assume a full set of HRIRs for all elevations.
837 /* Perform minimum-phase reconstruction using the magnitude responses of the
838 * HRIR set. Work is delegated to this struct, which runs asynchronously on one
839 * or more threads (sharing the same reconstructor object).
841 struct HrirReconstructor {
842 std::vector<al::span<double>> mIrs;
843 std::atomic<size_t> mCurrent{};
844 std::atomic<size_t> mDone{};
845 uint mFftSize{};
846 uint mIrPoints{};
848 void Worker()
850 auto h = std::vector<complex_d>(mFftSize);
851 auto mags = std::vector<double>(mFftSize);
852 size_t m{(mFftSize/2) + 1};
854 while(true)
856 /* Load the current index to process. */
857 size_t idx{mCurrent.load()};
858 do {
859 /* If the index is at the end, we're done. */
860 if(idx >= mIrs.size())
861 return;
862 /* Otherwise, increment the current index atomically so other
863 * threads know to go to the next one. If this call fails, the
864 * current index was just changed by another thread and the new
865 * value is loaded into idx, which we'll recheck.
867 } while(!mCurrent.compare_exchange_weak(idx, idx+1, std::memory_order_relaxed));
869 /* Now do the reconstruction, and apply the inverse FFT to get the
870 * time-domain response.
872 for(size_t i{0};i < m;++i)
873 mags[i] = std::max(mIrs[idx][i], Epsilon);
874 MinimumPhase(mags, h);
875 FftInverse(mFftSize, h.data());
876 for(uint i{0u};i < mIrPoints;++i)
877 mIrs[idx][i] = h[i].real();
879 /* Increment the number of IRs done. */
880 mDone.fetch_add(1);
885 void ReconstructHrirs(const HrirDataT *hData, const uint numThreads)
887 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
889 /* Set up the reconstructor with the needed size info and pointers to the
890 * IRs to process.
892 HrirReconstructor reconstructor;
893 reconstructor.mCurrent.store(0, std::memory_order_relaxed);
894 reconstructor.mDone.store(0, std::memory_order_relaxed);
895 reconstructor.mFftSize = hData->mFftSize;
896 reconstructor.mIrPoints = hData->mIrPoints;
897 for(const auto &field : hData->mFds)
899 for(auto &elev : field.mEvs)
901 for(const auto &azd : elev.mAzs)
903 for(uint ti{0u};ti < channels;ti++)
904 reconstructor.mIrs.push_back(azd.mIrs[ti]);
909 /* Launch threads to work on reconstruction. */
910 std::vector<std::thread> thrds;
911 thrds.reserve(numThreads);
912 for(size_t i{0};i < numThreads;++i)
913 thrds.emplace_back(std::mem_fn(&HrirReconstructor::Worker), &reconstructor);
915 /* Keep track of the number of IRs done, periodically reporting it. */
916 size_t count;
917 do {
918 std::this_thread::sleep_for(std::chrono::milliseconds{50});
920 count = reconstructor.mDone.load();
921 size_t pcdone{count * 100 / reconstructor.mIrs.size()};
923 fmt::print("\r{:3}% done ({} of {})", pcdone, count, reconstructor.mIrs.size());
924 fflush(stdout);
925 } while(count < reconstructor.mIrs.size());
926 fmt::println("");
928 for(auto &thrd : thrds)
930 if(thrd.joinable())
931 thrd.join();
935 // Normalize the HRIR set and slightly attenuate the result.
936 void NormalizeHrirs(HrirDataT *hData)
938 const uint channels{(hData->mChannelType == CT_STEREO) ? 2u : 1u};
939 const uint irSize{hData->mIrPoints};
941 /* Find the maximum amplitude and RMS out of all the IRs. */
942 struct LevelPair { double amp, rms; };
943 auto mesasure_channel = [irSize](const LevelPair levels, al::span<const double> ir)
945 /* Calculate the peak amplitude and RMS of this IR. */
946 ir = ir.first(irSize);
947 auto current = std::accumulate(ir.cbegin(), ir.cend(), LevelPair{0.0, 0.0},
948 [](const LevelPair cur, const double impulse)
950 return LevelPair{std::max(std::abs(impulse), cur.amp), cur.rms + impulse*impulse};
952 current.rms = std::sqrt(current.rms / irSize);
954 /* Accumulate levels by taking the maximum amplitude and RMS. */
955 return LevelPair{std::max(current.amp, levels.amp), std::max(current.rms, levels.rms)};
957 auto measure_azi = [channels,mesasure_channel](const LevelPair levels, const HrirAzT &azi)
958 { return std::accumulate(azi.mIrs.begin(), azi.mIrs.begin()+channels, levels, mesasure_channel); };
959 auto measure_elev = [measure_azi](const LevelPair levels, const HrirEvT &elev)
960 { return std::accumulate(elev.mAzs.cbegin(), elev.mAzs.cend(), levels, measure_azi); };
961 auto measure_field = [measure_elev](const LevelPair levels, const HrirFdT &field)
962 { return std::accumulate(field.mEvs.cbegin(), field.mEvs.cend(), levels, measure_elev); };
964 const auto maxlev = std::accumulate(hData->mFds.begin(), hData->mFds.end(),
965 LevelPair{0.0, 0.0}, measure_field);
967 /* Normalize using the maximum RMS of the HRIRs. The RMS measure for the
968 * non-filtered signal is of an impulse with equal length (to the filter):
970 * rms_impulse = sqrt(sum([ 1^2, 0^2, 0^2, ... ]) / n)
971 * = sqrt(1 / n)
973 * This helps keep a more consistent volume between the non-filtered signal
974 * and various data sets.
976 double factor{std::sqrt(1.0 / irSize) / maxlev.rms};
978 /* Also ensure the samples themselves won't clip. */
979 factor = std::min(factor, 0.99/maxlev.amp);
981 /* Now scale all IRs by the given factor. */
982 auto proc_channel = [irSize,factor](al::span<double> ir)
984 ir = ir.first(irSize);
985 std::transform(ir.cbegin(), ir.cend(), ir.begin(),
986 [factor](double s) { return s * factor; });
988 auto proc_azi = [channels,proc_channel](HrirAzT &azi)
989 { std::for_each(azi.mIrs.begin(), azi.mIrs.begin()+channels, proc_channel); };
990 auto proc_elev = [proc_azi](HrirEvT &elev)
991 { std::for_each(elev.mAzs.begin(), elev.mAzs.end(), proc_azi); };
992 auto proc1_field = [proc_elev](HrirFdT &field)
993 { std::for_each(field.mEvs.begin(), field.mEvs.end(), proc_elev); };
995 std::for_each(hData->mFds.begin(), hData->mFds.end(), proc1_field);
998 // Calculate the left-ear time delay using a spherical head model.
999 double CalcLTD(const double ev, const double az, const double rad, const double dist)
1001 double azp, dlp, l, al;
1003 azp = std::asin(std::cos(ev) * std::sin(az));
1004 dlp = std::sqrt((dist*dist) + (rad*rad) + (2.0*dist*rad*sin(azp)));
1005 l = std::sqrt((dist*dist) - (rad*rad));
1006 al = (0.5 * al::numbers::pi) + azp;
1007 if(dlp > l)
1008 dlp = l + (rad * (al - std::acos(rad / dist)));
1009 return dlp / 343.3;
1012 // Calculate the effective head-related time delays for each minimum-phase
1013 // HRIR. This is done per-field since distance delay is ignored.
1014 void CalculateHrtds(const HeadModelT model, const double radius, HrirDataT *hData)
1016 uint channels = (hData->mChannelType == CT_STEREO) ? 2 : 1;
1017 double customRatio{radius / hData->mRadius};
1018 uint ti;
1020 if(model == HM_Sphere)
1022 for(auto &field : hData->mFds)
1024 for(auto &elev : field.mEvs)
1026 for(auto &azd : elev.mAzs)
1028 for(ti = 0;ti < channels;ti++)
1029 azd.mDelays[ti] = CalcLTD(elev.mElevation, azd.mAzimuth, radius, field.mDistance);
1034 else if(customRatio != 1.0)
1036 for(auto &field : hData->mFds)
1038 for(auto &elev : field.mEvs)
1040 for(auto &azd : elev.mAzs)
1042 for(ti = 0;ti < channels;ti++)
1043 azd.mDelays[ti] *= customRatio;
1049 double maxHrtd{0.0};
1050 for(auto &field : hData->mFds)
1052 double minHrtd{std::numeric_limits<double>::infinity()};
1053 for(auto &elev : field.mEvs)
1055 for(auto &azd : elev.mAzs)
1057 for(ti = 0;ti < channels;ti++)
1058 minHrtd = std::min(azd.mDelays[ti], minHrtd);
1062 for(auto &elev : field.mEvs)
1064 for(auto &azd : elev.mAzs)
1066 for(ti = 0;ti < channels;ti++)
1068 azd.mDelays[ti] = (azd.mDelays[ti]-minHrtd) * hData->mIrRate;
1069 maxHrtd = std::max(maxHrtd, azd.mDelays[ti]);
1074 if(maxHrtd > MaxHrtd)
1076 fmt::println(" Scaling for max delay of {:f} samples to {:f}\n...", maxHrtd, MaxHrtd);
1077 const double scale{MaxHrtd / maxHrtd};
1078 for(auto &field : hData->mFds)
1080 for(auto &elev : field.mEvs)
1082 for(auto &azd : elev.mAzs)
1084 for(ti = 0;ti < channels;ti++)
1085 azd.mDelays[ti] *= scale;
1092 } // namespace
1094 // Allocate and configure dynamic HRIR structures.
1095 bool PrepareHrirData(const al::span<const double> distances,
1096 const al::span<const uint,MAX_FD_COUNT> evCounts,
1097 const al::span<const std::array<uint,MAX_EV_COUNT>,MAX_FD_COUNT> azCounts, HrirDataT *hData)
1099 uint evTotal{0}, azTotal{0};
1101 for(size_t fi{0};fi < distances.size();++fi)
1103 evTotal += evCounts[fi];
1104 for(size_t ei{0};ei < evCounts[fi];++ei)
1105 azTotal += azCounts[fi][ei];
1107 if(!evTotal || !azTotal)
1108 return false;
1110 hData->mEvsBase.resize(evTotal);
1111 hData->mAzsBase.resize(azTotal);
1112 hData->mFds.resize(distances.size());
1113 hData->mIrCount = azTotal;
1114 evTotal = 0;
1115 azTotal = 0;
1116 for(size_t fi{0};fi < distances.size();++fi)
1118 hData->mFds[fi].mDistance = distances[fi];
1119 hData->mFds[fi].mEvStart = 0;
1120 hData->mFds[fi].mEvs = al::span{hData->mEvsBase}.subspan(evTotal, evCounts[fi]);
1121 evTotal += evCounts[fi];
1122 for(uint ei{0};ei < evCounts[fi];++ei)
1124 uint azCount = azCounts[fi][ei];
1126 hData->mFds[fi].mEvs[ei].mElevation = -al::numbers::pi / 2.0 + al::numbers::pi * ei /
1127 (evCounts[fi] - 1);
1128 hData->mFds[fi].mEvs[ei].mAzs = al::span{hData->mAzsBase}.subspan(azTotal, azCount);
1129 for(uint ai{0};ai < azCount;ai++)
1131 hData->mFds[fi].mEvs[ei].mAzs[ai].mAzimuth = 2.0 * al::numbers::pi * ai / azCount;
1132 hData->mFds[fi].mEvs[ei].mAzs[ai].mIndex = azTotal + ai;
1133 hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[0] = 0.0;
1134 hData->mFds[fi].mEvs[ei].mAzs[ai].mDelays[1] = 0.0;
1135 hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[0] = {};
1136 hData->mFds[fi].mEvs[ei].mAzs[ai].mIrs[1] = {};
1138 azTotal += azCount;
1141 return true;
1145 namespace {
1147 /* Parse the data set definition and process the source data, storing the
1148 * resulting data set as desired. If the input name is NULL it will read
1149 * from standard input.
1151 bool ProcessDefinition(std::string_view inName, const uint outRate, const ChannelModeT chanMode,
1152 const bool farfield, const uint numThreads, const uint fftSize, const bool equalize,
1153 const bool surface, const double limit, const uint truncSize, const HeadModelT model,
1154 const double radius, const std::string_view outName)
1156 HrirDataT hData;
1158 fmt::println("Using {} thread{}.", numThreads, (numThreads==1)?"":"s");
1159 if(inName.empty() || inName == "-"sv)
1161 inName = "stdin"sv;
1162 fmt::println("Reading HRIR definition from {}...", inName);
1163 if(!LoadDefInput(std::cin, {}, inName, fftSize, truncSize, outRate, chanMode, &hData))
1164 return false;
1166 else
1168 auto input = std::make_unique<std::ifstream>(std::filesystem::u8path(inName));
1169 if(!input->is_open())
1171 fmt::println(stderr, "Error: Could not open input file '{}'", inName);
1172 return false;
1175 std::array<char,4> startbytes{};
1176 input->read(startbytes.data(), startbytes.size());
1177 if(input->gcount() != startbytes.size() || !input->good())
1179 fmt::println(stderr, "Error: Could not read input file '{}'", inName);
1180 return false;
1183 if(startbytes[0] == '\x89' && startbytes[1] == 'H' && startbytes[2] == 'D'
1184 && startbytes[3] == 'F')
1186 input = nullptr;
1187 fmt::println("Reading HRTF data from {}...", inName);
1188 if(!LoadSofaFile(inName, numThreads, fftSize, truncSize, outRate, chanMode, &hData))
1189 return false;
1191 else
1193 fmt::println("Reading HRIR definition from {}...", inName);
1194 if(!LoadDefInput(*input, startbytes, inName, fftSize, truncSize, outRate, chanMode,
1195 &hData))
1196 return false;
1200 if(equalize)
1202 uint c{(hData.mChannelType == CT_STEREO) ? 2u : 1u};
1203 uint m{hData.mFftSize/2u + 1u};
1204 auto dfa = std::vector<double>(size_t{c} * m);
1206 if(hData.mFds.size() > 1)
1208 fmt::println("Balancing field magnitudes...");
1209 BalanceFieldMagnitudes(&hData, c, m);
1211 fmt::println("Calculating diffuse-field average...");
1212 CalculateDiffuseFieldAverage(&hData, c, m, surface, limit, dfa);
1213 fmt::println("Performing diffuse-field equalization...");
1214 DiffuseFieldEqualize(c, m, dfa, &hData);
1216 if(hData.mFds.size() > 1)
1218 fmt::println("Sorting {} fields...", hData.mFds.size());
1219 std::sort(hData.mFds.begin(), hData.mFds.end(),
1220 [](const HrirFdT &lhs, const HrirFdT &rhs) noexcept
1221 { return lhs.mDistance < rhs.mDistance; });
1222 if(farfield)
1224 fmt::println("Clearing {} near field{}...", hData.mFds.size()-1,
1225 (hData.mFds.size()-1 != 1) ? "s" : "");
1226 hData.mFds.erase(hData.mFds.cbegin(), hData.mFds.cend()-1);
1229 fmt::println("Synthesizing missing elevations...");
1230 if(model == HM_Dataset)
1231 SynthesizeOnsets(&hData);
1232 SynthesizeHrirs(&hData);
1233 fmt::println("Performing minimum phase reconstruction...");
1234 ReconstructHrirs(&hData, numThreads);
1235 fmt::println("Truncating minimum-phase HRIRs...");
1236 hData.mIrPoints = truncSize;
1237 fmt::println("Normalizing final HRIRs...");
1238 NormalizeHrirs(&hData);
1239 fmt::println("Calculating impulse delays...");
1240 CalculateHrtds(model, (radius > DefaultCustomRadius) ? radius : hData.mRadius, &hData);
1242 const auto rateStr = std::to_string(hData.mIrRate);
1243 const auto expName = StrSubst(outName, "%r"sv, rateStr);
1244 fmt::println("Creating MHR data set {}...", expName);
1245 return StoreMhr(&hData, expName);
1248 void PrintHelp(const std::string_view argv0, FILE *ofile)
1250 fmt::println(ofile, "Usage: {} [<option>...]\n", argv0);
1251 fmt::println(ofile, "Options:");
1252 fmt::println(ofile, " -r <rate> Change the data set sample rate to the specified value and");
1253 fmt::println(ofile, " resample the HRIRs accordingly.");
1254 fmt::println(ofile, " -m Change the data set to mono, mirroring the left ear for the");
1255 fmt::println(ofile, " right ear.");
1256 fmt::println(ofile, " -a Change the data set to single field, using the farthest field.");
1257 fmt::println(ofile, " -j <threads> Number of threads used to process HRIRs (default: 2).");
1258 fmt::println(ofile, " -f <points> Override the FFT window size (default: {}).", DefaultFftSize);
1259 fmt::println(ofile, " -e {{on|off}} Toggle diffuse-field equalization (default: {}).", (DefaultEqualize ? "on" : "off"));
1260 fmt::println(ofile, " -s {{on|off}} Toggle surface-weighted diffuse-field average (default: {}).", (DefaultSurface ? "on" : "off"));
1261 fmt::println(ofile, " -l {{<dB>|none}} Specify a limit to the magnitude range of the diffuse-field");
1262 fmt::println(ofile, " average (default: {:.2f}).", DefaultLimit);
1263 fmt::println(ofile, " -w <points> Specify the size of the truncation window that's applied");
1264 fmt::println(ofile, " after minimum-phase reconstruction (default: {}).", DefaultTruncSize);
1265 fmt::println(ofile, " -d {{dataset| Specify the model used for calculating the head-delay timing");
1266 fmt::println(ofile, " sphere}} values (default: {}).", ((HM_Default == HM_Dataset) ? "dataset" : "sphere"));
1267 fmt::println(ofile, " -c <radius> Use a customized head radius measured to-ear in meters.");
1268 fmt::println(ofile, " -i <filename> Specify an HRIR definition file to use (defaults to stdin).");
1269 fmt::println(ofile, " -o <filename> Specify an output file. Use of '%r' will be substituted with");
1270 fmt::println(ofile, " the data set sample rate.");
1273 // Standard command line dispatch.
1274 int main(al::span<std::string_view> args)
1276 if(args.size() < 2)
1278 fmt::println("HRTF Processing and Composition Utility\n");
1279 PrintHelp(args[0], stdout);
1280 exit(EXIT_SUCCESS);
1283 std::string_view outName{"./oalsoft_hrtf_%r.mhr"sv};
1284 uint outRate{0};
1285 ChannelModeT chanMode{CM_AllowStereo};
1286 uint fftSize{DefaultFftSize};
1287 bool equalize{DefaultEqualize};
1288 bool surface{DefaultSurface};
1289 double limit{DefaultLimit};
1290 uint numThreads{2};
1291 uint truncSize{DefaultTruncSize};
1292 HeadModelT model{HM_Default};
1293 double radius{DefaultCustomRadius};
1294 bool farfield{false};
1295 std::string_view inName;
1297 const std::string_view optlist{"r:maj:f:e:s:l:w:d:c:e:i:o:h"sv};
1298 const auto arg0 = args[0];
1299 args = args.subspan(1);
1300 std::string_view optarg;
1301 size_t argplace{0};
1303 auto getarg = [&args,&argplace,&optarg,optlist]
1305 while(!args.empty() && argplace >= args[0].size())
1307 argplace = 0;
1308 args = args.subspan(1);
1310 if(args.empty())
1311 return 0;
1313 if(argplace == 0)
1315 if(args[0] == "--"sv)
1316 return 0;
1318 if(args[0][0] != '-' || args[0].size() == 1)
1320 fmt::println(stderr, "Invalid argument: {}", args[0]);
1321 return -1;
1323 ++argplace;
1326 const char nextopt{args[0][argplace]};
1327 const auto listidx = optlist.find(nextopt);
1328 if(listidx >= optlist.size())
1330 fmt::println(stderr, "Unknown argument: -{:c}", nextopt);
1331 return -1;
1333 const bool needsarg{listidx+1 < optlist.size() && optlist[listidx+1] == ':'};
1334 if(needsarg && (argplace+1 < args[0].size() || args.size() < 2))
1336 fmt::println(stderr, "Missing parameter for argument: -{:c}", nextopt);
1337 return -1;
1339 if(++argplace == args[0].size())
1341 if(needsarg)
1342 optarg = args[1];
1343 argplace = 0;
1344 args = args.subspan(1u + needsarg);
1347 return int{nextopt};
1350 while(auto opt = getarg())
1352 std::size_t endpos{};
1353 switch(opt)
1355 case 'r':
1356 outRate = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1357 if(endpos != optarg.size() || outRate < MIN_RATE || outRate > MAX_RATE)
1359 fmt::println(stderr,
1360 "\nError: Got unexpected value \"{}\" for option -{:c}, expected between {} to {}.",
1361 optarg, opt, MIN_RATE, MAX_RATE);
1362 exit(EXIT_FAILURE);
1364 break;
1366 case 'm':
1367 chanMode = CM_ForceMono;
1368 break;
1370 case 'a':
1371 farfield = true;
1372 break;
1374 case 'j':
1375 numThreads = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1376 if(endpos != optarg.size() || numThreads > 64)
1378 fmt::println(stderr,
1379 "\nError: Got unexpected value \"{}\" for option -{:c}, expected between {} to {}.",
1380 optarg, opt, 0, 64);
1381 exit(EXIT_FAILURE);
1383 if(numThreads == 0)
1384 numThreads = std::thread::hardware_concurrency();
1385 break;
1387 case 'f':
1388 fftSize = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1389 if(endpos != optarg.size() || (fftSize&(fftSize-1)) || fftSize < MinFftSize
1390 || fftSize > MaxFftSize)
1392 fmt::println(stderr,
1393 "\nError: Got unexpected value \"{}\" for option -{:c}, expected a power-of-two between {} to {}.",
1394 optarg, opt, MinFftSize, MaxFftSize);
1395 exit(EXIT_FAILURE);
1397 break;
1399 case 'e':
1400 if(optarg == "on"sv)
1401 equalize = true;
1402 else if(optarg == "off"sv)
1403 equalize = false;
1404 else
1406 fmt::println(stderr,
1407 "\nError: Got unexpected value \"{}\" for option -{:c}, expected on or off.",
1408 optarg, opt);
1409 exit(EXIT_FAILURE);
1411 break;
1413 case 's':
1414 if(optarg == "on"sv)
1415 surface = true;
1416 else if(optarg == "off"sv)
1417 surface = false;
1418 else
1420 fmt::println(stderr,
1421 "\nError: Got unexpected value \"{}\" for option -{:c}, expected on or off.",
1422 optarg, 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 fmt::println(stderr,
1436 "\nError: Got unexpected value \"{}\" for option -{:c}, expected between {:.0f} to {:.0f}.",
1437 optarg, opt, MinLimit, MaxLimit);
1438 exit(EXIT_FAILURE);
1441 break;
1443 case 'w':
1444 truncSize = static_cast<uint>(std::stoul(std::string{optarg}, &endpos, 10));
1445 if(endpos != optarg.size() || truncSize < MinTruncSize || truncSize > MaxTruncSize)
1447 fmt::println(stderr,
1448 "\nError: Got unexpected value \"{}\" for option -{:c}, expected between {} to {}.",
1449 optarg, opt, MinTruncSize, MaxTruncSize);
1450 exit(EXIT_FAILURE);
1452 break;
1454 case 'd':
1455 if(optarg == "dataset"sv)
1456 model = HM_Dataset;
1457 else if(optarg == "sphere"sv)
1458 model = HM_Sphere;
1459 else
1461 fmt::println(stderr,
1462 "\nError: Got unexpected value \"{}\" for option -{:c}, expected dataset or sphere.",
1463 optarg, 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 fmt::println(stderr,
1473 "\nError: Got unexpected value \"{}\" for option -{:c}, expected between {:.2f} to {:.2f}.",
1474 optarg, opt, MinCustomRadius, MaxCustomRadius);
1475 exit(EXIT_FAILURE);
1477 break;
1479 case 'i':
1480 inName = optarg;
1481 break;
1483 case 'o':
1484 outName = optarg;
1485 break;
1487 case 'h':
1488 PrintHelp(arg0, stdout);
1489 exit(EXIT_SUCCESS);
1491 default: /* '?' */
1492 PrintHelp(arg0, stderr);
1493 exit(EXIT_FAILURE);
1497 const int ret{ProcessDefinition(inName, outRate, chanMode, farfield, numThreads, fftSize,
1498 equalize, surface, limit, truncSize, model, radius, outName)};
1499 if(!ret) return -1;
1500 fmt::println("Operation completed.");
1502 return EXIT_SUCCESS;
1505 } /* namespace */
1507 int main(int argc, char **argv)
1509 assert(argc >= 0);
1510 auto args = std::vector<std::string_view>(static_cast<unsigned int>(argc));
1511 std::copy_n(argv, args.size(), args.begin());
1512 return main(al::span{args});