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
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
48 * Modeling Interaural Time Difference Assuming a Spherical Head
50 * Music 150, Musical Acoustics, Stanford University
53 * The formulae for calculating the Kaiser window metrics are from the
56 * Discrete-Time Signal Processing
57 * Alan V. Oppenheim and Ronald W. Schafer
58 * Prentice-Hall Signal Processing Series
62 #define _UNICODE /* NOLINT(bugprone-reserved-identifier) */
83 #include <string_view>
87 #include "alcomplex.h"
88 #include "alnumbers.h"
89 #include "alnumeric.h"
96 #include "win_main_utf8.h"
99 HrirDataT::~HrirDataT() = default;
103 using namespace std::string_view_literals
;
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.
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
{
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
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());
181 while(endpos
< in
.size() && std::toupper(in
[endpos
]) != std::toupper(pat
.front()))
183 ret
+= in
.substr(0, endpos
);
184 in
= in
.substr(endpos
);
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;
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
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;
240 for(uint i
{lower
};i
<= upper
;++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
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};
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.
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
);
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
);
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
);
322 if(!WriteAscii(GetMHRMarker(), ostream
, filename
))
324 if(!WriteBin4(4, hData
->mIrRate
, ostream
, filename
))
326 if(!WriteBin4(1, static_cast<uint32_t>(hData
->mChannelType
), ostream
, filename
))
328 if(!WriteBin4(1, hData
->mIrPoints
, ostream
, filename
))
330 if(!WriteBin4(1, static_cast<uint
>(hData
->mFds
.size()), ostream
, filename
))
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
))
337 if(!WriteBin4(1, static_cast<uint32_t>(hData
->mFds
[fi
].mEvs
.size()), ostream
, filename
))
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
))
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
))
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;
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
{};
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
;
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.
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
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.
487 for(fi
= 0;fi
< hData
->mFds
.size();fi
++)
489 // Normalize the weights given the total surface coverage for all
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
);
509 // Use coverage weighting to calculate the average.
510 CalculateDfWeights(hData
, weights
);
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
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())};
595 *a1
= (i
+ 1) % static_cast<uint
>(field
.mEvs
[ei
].mAzs
.size());
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.
622 /* Take the polar opposite position of the desired measurement and
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
++)
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
);
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
++)
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
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
++)
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
),
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
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
};
740 for(uint ti
{0u};ti
< channels
;ti
++)
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
);
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
);
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
++)
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
);
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
);
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
{};
850 auto h
= std::vector
<complex_d
>(mFftSize
);
851 auto mags
= std::vector
<double>(mFftSize
);
852 size_t m
{(mFftSize
/2) + 1};
856 /* Load the current index to process. */
857 size_t idx
{mCurrent
.load()};
859 /* If the index is at the end, we're done. */
860 if(idx
>= mIrs
.size())
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. */
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
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. */
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());
925 } while(count
< reconstructor
.mIrs
.size());
928 for(auto &thrd
: thrds
)
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)
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
;
1008 dlp
= l
+ (rad
* (al
- std::acos(rad
/ dist
)));
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
};
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
;
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
)
1110 hData
->mEvsBase
.resize(evTotal
);
1111 hData
->mAzsBase
.resize(azTotal
);
1112 hData
->mFds
.resize(distances
.size());
1113 hData
->mIrCount
= azTotal
;
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
/
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] = {};
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
)
1158 fmt::println("Using {} thread{}.", numThreads
, (numThreads
==1)?"":"s");
1159 if(inName
.empty() || inName
== "-"sv
)
1162 fmt::println("Reading HRIR definition from {}...", inName
);
1163 if(!LoadDefInput(std::cin
, {}, inName
, fftSize
, truncSize
, outRate
, chanMode
, &hData
))
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
);
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
);
1183 if(startbytes
[0] == '\x89' && startbytes
[1] == 'H' && startbytes
[2] == 'D'
1184 && startbytes
[3] == 'F')
1187 fmt::println("Reading HRTF data from {}...", inName
);
1188 if(!LoadSofaFile(inName
, numThreads
, fftSize
, truncSize
, outRate
, chanMode
, &hData
))
1193 fmt::println("Reading HRIR definition from {}...", inName
);
1194 if(!LoadDefInput(*input
, startbytes
, inName
, fftSize
, truncSize
, outRate
, chanMode
,
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
; });
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
)
1278 fmt::println("HRTF Processing and Composition Utility\n");
1279 PrintHelp(args
[0], stdout
);
1283 std::string_view outName
{"./oalsoft_hrtf_%r.mhr"sv
};
1285 ChannelModeT chanMode
{CM_AllowStereo
};
1286 uint fftSize
{DefaultFftSize
};
1287 bool equalize
{DefaultEqualize
};
1288 bool surface
{DefaultSurface
};
1289 double limit
{DefaultLimit
};
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
;
1303 auto getarg
= [&args
,&argplace
,&optarg
,optlist
]
1305 while(!args
.empty() && argplace
>= args
[0].size())
1308 args
= args
.subspan(1);
1315 if(args
[0] == "--"sv
)
1318 if(args
[0][0] != '-' || args
[0].size() == 1)
1320 fmt::println(stderr
, "Invalid argument: {}", args
[0]);
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
);
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
);
1339 if(++argplace
== args
[0].size())
1344 args
= args
.subspan(1u + needsarg
);
1347 return int{nextopt
};
1350 while(auto opt
= getarg())
1352 std::size_t endpos
{};
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
);
1367 chanMode
= CM_ForceMono
;
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);
1384 numThreads
= std::thread::hardware_concurrency();
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
);
1400 if(optarg
== "on"sv
)
1402 else if(optarg
== "off"sv
)
1406 fmt::println(stderr
,
1407 "\nError: Got unexpected value \"{}\" for option -{:c}, expected on or off.",
1414 if(optarg
== "on"sv
)
1416 else if(optarg
== "off"sv
)
1420 fmt::println(stderr
,
1421 "\nError: Got unexpected value \"{}\" for option -{:c}, expected on or off.",
1428 if(optarg
== "none"sv
)
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
);
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
);
1455 if(optarg
== "dataset"sv
)
1457 else if(optarg
== "sphere"sv
)
1461 fmt::println(stderr
,
1462 "\nError: Got unexpected value \"{}\" for option -{:c}, expected dataset or sphere.",
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
);
1488 PrintHelp(arg0
, stdout
);
1492 PrintHelp(arg0
, stderr
);
1497 const int ret
{ProcessDefinition(inName
, outRate
, chanMode
, farfield
, numThreads
, fftSize
,
1498 equalize
, surface
, limit
, truncSize
, model
, radius
, outName
)};
1500 fmt::println("Operation completed.");
1502 return EXIT_SUCCESS
;
1507 int main(int argc
, char **argv
)
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
});