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>
88 #include "alcomplex.h"
89 #include "alnumbers.h"
90 #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 fprintf(stderr
, "\nError: Bad write to file '%.*s'.\n", al::sizei(filename
),
293 // Write a binary value of the given byte order and byte size to a file,
294 // loading it from a 32-bit unsigned integer.
295 auto WriteBin4(const uint bytes
, const uint32_t in
, std::ostream
&ostream
,
296 const std::string_view filename
) -> int
298 std::array
<char,4> out
{};
299 for(uint i
{0};i
< bytes
;i
++)
300 out
[i
] = static_cast<char>((in
>>(i
*8)) & 0x000000FF);
302 if(!ostream
.write(out
.data(), std::streamsize(bytes
)) || ostream
.bad())
304 fprintf(stderr
, "\nError: Bad write to file '%.*s'.\n", al::sizei(filename
),
311 // Store the OpenAL Soft HRTF data set.
312 auto StoreMhr(const HrirDataT
*hData
, const std::string_view filename
) -> bool
314 const uint channels
{(hData
->mChannelType
== CT_STEREO
) ? 2u : 1u};
315 const uint n
{hData
->mIrPoints
};
316 uint dither_seed
{22222};
318 auto ostream
= std::ofstream
{std::filesystem::u8path(filename
), std::ios::binary
};
319 if(!ostream
.is_open())
321 fprintf(stderr
, "\nError: Could not open MHR file '%.*s'.\n", al::sizei(filename
),
325 if(!WriteAscii(GetMHRMarker(), ostream
, filename
))
327 if(!WriteBin4(4, hData
->mIrRate
, ostream
, filename
))
329 if(!WriteBin4(1, static_cast<uint32_t>(hData
->mChannelType
), ostream
, filename
))
331 if(!WriteBin4(1, hData
->mIrPoints
, ostream
, filename
))
333 if(!WriteBin4(1, static_cast<uint
>(hData
->mFds
.size()), ostream
, filename
))
335 for(size_t fi
{hData
->mFds
.size()-1};fi
< hData
->mFds
.size();--fi
)
337 auto fdist
= static_cast<uint32_t>(std::round(1000.0 * hData
->mFds
[fi
].mDistance
));
338 if(!WriteBin4(2, fdist
, ostream
, filename
))
340 if(!WriteBin4(1, static_cast<uint32_t>(hData
->mFds
[fi
].mEvs
.size()), ostream
, filename
))
342 for(size_t ei
{0};ei
< hData
->mFds
[fi
].mEvs
.size();++ei
)
344 const auto &elev
= hData
->mFds
[fi
].mEvs
[ei
];
345 if(!WriteBin4(1, static_cast<uint32_t>(elev
.mAzs
.size()), ostream
, filename
))
350 for(size_t fi
{hData
->mFds
.size()-1};fi
< hData
->mFds
.size();--fi
)
352 static constexpr double scale
{8388607.0};
353 static constexpr uint bps
{3u};
355 for(const auto &evd
: hData
->mFds
[fi
].mEvs
)
357 for(const auto &azd
: evd
.mAzs
)
359 std::array
<double,MaxTruncSize
*2_uz
> out
{};
361 TpdfDither(out
, azd
.mIrs
[0].first(n
), scale
, 0, channels
, &dither_seed
);
362 if(hData
->mChannelType
== CT_STEREO
)
363 TpdfDither(out
, azd
.mIrs
[1].first(n
), scale
, 1, channels
, &dither_seed
);
364 const size_t numsamples
{size_t{channels
} * n
};
365 for(size_t i
{0};i
< numsamples
;i
++)
367 const auto v
= static_cast<int>(Clamp(out
[i
], -scale
-1.0, scale
));
368 if(!WriteBin4(bps
, static_cast<uint32_t>(v
), ostream
, filename
))
374 for(size_t fi
{hData
->mFds
.size()-1};fi
< hData
->mFds
.size();--fi
)
376 /* Delay storage has 2 bits of extra precision. */
377 static constexpr double DelayPrecScale
{4.0};
378 for(const auto &evd
: hData
->mFds
[fi
].mEvs
)
380 for(const auto &azd
: evd
.mAzs
)
382 auto v
= static_cast<uint
>(std::round(azd
.mDelays
[0]*DelayPrecScale
));
383 if(!WriteBin4(1, v
, ostream
, filename
)) return false;
384 if(hData
->mChannelType
== CT_STEREO
)
386 v
= static_cast<uint
>(std::round(azd
.mDelays
[1]*DelayPrecScale
));
387 if(!WriteBin4(1, v
, ostream
, filename
)) return false;
396 /***********************
397 *** HRTF processing ***
398 ***********************/
400 /* Balances the maximum HRIR magnitudes of multi-field data sets by
401 * independently normalizing each field in relation to the overall maximum.
402 * This is done to ignore distance attenuation.
404 void BalanceFieldMagnitudes(const HrirDataT
*hData
, const uint channels
, const uint m
)
406 std::array
<double,MAX_FD_COUNT
> maxMags
{};
409 for(size_t fi
{0};fi
< hData
->mFds
.size();++fi
)
411 for(size_t ei
{hData
->mFds
[fi
].mEvStart
};ei
< hData
->mFds
[fi
].mEvs
.size();++ei
)
413 for(const auto &azd
: hData
->mFds
[fi
].mEvs
[ei
].mAzs
)
415 for(size_t ti
{0};ti
< channels
;++ti
)
417 for(size_t i
{0};i
< m
;++i
)
418 maxMags
[fi
] = std::max(azd
.mIrs
[ti
][i
], maxMags
[fi
]);
423 maxMag
= std::max(maxMags
[fi
], maxMag
);
426 for(size_t fi
{0};fi
< hData
->mFds
.size();++fi
)
428 const double magFactor
{maxMag
/ maxMags
[fi
]};
430 for(size_t ei
{hData
->mFds
[fi
].mEvStart
};ei
< hData
->mFds
[fi
].mEvs
.size();++ei
)
432 for(const auto &azd
: hData
->mFds
[fi
].mEvs
[ei
].mAzs
)
434 for(size_t ti
{0};ti
< channels
;++ti
)
436 for(size_t i
{0};i
< m
;++i
)
437 azd
.mIrs
[ti
][i
] *= magFactor
;
444 /* Calculate the contribution of each HRIR to the diffuse-field average based
445 * on its coverage volume. All volumes are centered at the spherical HRIR
446 * coordinates and measured by extruded solid angle.
448 void CalculateDfWeights(const HrirDataT
*hData
, const al::span
<double> weights
)
450 double sum
, innerRa
, outerRa
, evs
, ev
, upperEv
, lowerEv
;
451 double solidAngle
, solidVolume
;
455 // The head radius acts as the limit for the inner radius.
456 innerRa
= hData
->mRadius
;
457 for(fi
= 0;fi
< hData
->mFds
.size();fi
++)
459 // Each volume ends half way between progressive field measurements.
460 if((fi
+ 1) < hData
->mFds
.size())
461 outerRa
= 0.5f
* (hData
->mFds
[fi
].mDistance
+ hData
->mFds
[fi
+ 1].mDistance
);
462 // The final volume has its limit extended to some practical value.
463 // This is done to emphasize the far-field responses in the average.
467 const double raPowDiff
{std::pow(outerRa
, 3.0) - std::pow(innerRa
, 3.0)};
468 evs
= al::numbers::pi
/ 2.0 / static_cast<double>(hData
->mFds
[fi
].mEvs
.size() - 1);
469 for(ei
= hData
->mFds
[fi
].mEvStart
;ei
< hData
->mFds
[fi
].mEvs
.size();ei
++)
471 const auto &elev
= hData
->mFds
[fi
].mEvs
[ei
];
472 // For each elevation, calculate the upper and lower limits of
474 ev
= elev
.mElevation
;
475 lowerEv
= std::max(-al::numbers::pi
/ 2.0, ev
- evs
);
476 upperEv
= std::min(al::numbers::pi
/ 2.0, ev
+ evs
);
477 // Calculate the surface area of the patch band.
478 solidAngle
= 2.0 * al::numbers::pi
* (std::sin(upperEv
) - std::sin(lowerEv
));
479 // Then the volume of the extruded patch band.
480 solidVolume
= solidAngle
* raPowDiff
/ 3.0;
481 // Each weight is the volume of one extruded patch.
482 weights
[(fi
*MAX_EV_COUNT
) + ei
] = solidVolume
/ static_cast<double>(elev
.mAzs
.size());
483 // Sum the total coverage volume of the HRIRs for all fields.
490 for(fi
= 0;fi
< hData
->mFds
.size();fi
++)
492 // Normalize the weights given the total surface coverage for all
494 for(ei
= hData
->mFds
[fi
].mEvStart
;ei
< hData
->mFds
[fi
].mEvs
.size();ei
++)
495 weights
[(fi
* MAX_EV_COUNT
) + ei
] /= sum
;
499 /* Calculate the diffuse-field average from the given magnitude responses of
500 * the HRIR set. Weighting can be applied to compensate for the varying
501 * coverage of each HRIR. The final average can then be limited by the
502 * specified magnitude range (in positive dB; 0.0 to skip).
504 void CalculateDiffuseFieldAverage(const HrirDataT
*hData
, const uint channels
, const uint m
,
505 const bool weighted
, const double limit
, const al::span
<double> dfa
)
507 std::vector
<double> weights(hData
->mFds
.size() * MAX_EV_COUNT
);
512 // Use coverage weighting to calculate the average.
513 CalculateDfWeights(hData
, weights
);
519 // If coverage weighting is not used, the weights still need to be
520 // averaged by the number of existing HRIRs.
521 count
= hData
->mIrCount
;
522 for(size_t fi
{0};fi
< hData
->mFds
.size();++fi
)
524 for(size_t ei
{0};ei
< hData
->mFds
[fi
].mEvStart
;++ei
)
525 count
-= static_cast<uint
>(hData
->mFds
[fi
].mEvs
[ei
].mAzs
.size());
527 weight
= 1.0 / count
;
529 for(size_t fi
{0};fi
< hData
->mFds
.size();++fi
)
531 for(size_t ei
{hData
->mFds
[fi
].mEvStart
};ei
< hData
->mFds
[fi
].mEvs
.size();++ei
)
532 weights
[(fi
* MAX_EV_COUNT
) + ei
] = weight
;
535 for(size_t ti
{0};ti
< channels
;++ti
)
537 for(size_t i
{0};i
< m
;++i
)
538 dfa
[(ti
* m
) + i
] = 0.0;
539 for(size_t fi
{0};fi
< hData
->mFds
.size();++fi
)
541 for(size_t ei
{hData
->mFds
[fi
].mEvStart
};ei
< hData
->mFds
[fi
].mEvs
.size();++ei
)
543 for(size_t ai
{0};ai
< hData
->mFds
[fi
].mEvs
[ei
].mAzs
.size();++ai
)
545 HrirAzT
*azd
= &hData
->mFds
[fi
].mEvs
[ei
].mAzs
[ai
];
546 // Get the weight for this HRIR's contribution.
547 double weight
= weights
[(fi
* MAX_EV_COUNT
) + ei
];
549 // Add this HRIR's weighted power average to the total.
550 for(size_t i
{0};i
< m
;++i
)
551 dfa
[(ti
* m
) + i
] += weight
* azd
->mIrs
[ti
][i
] * azd
->mIrs
[ti
][i
];
555 // Finish the average calculation and keep it from being too small.
556 for(size_t i
{0};i
< m
;++i
)
557 dfa
[(ti
* m
) + i
] = std::max(sqrt(dfa
[(ti
* m
) + i
]), Epsilon
);
558 // Apply a limit to the magnitude range of the diffuse-field average
561 LimitMagnitudeResponse(hData
->mFftSize
, m
, limit
, dfa
.subspan(ti
* m
));
565 // Perform diffuse-field equalization on the magnitude responses of the HRIR
566 // set using the given average response.
567 void DiffuseFieldEqualize(const uint channels
, const uint m
, const al::span
<const double> dfa
,
568 const HrirDataT
*hData
)
570 for(size_t fi
{0};fi
< hData
->mFds
.size();++fi
)
572 for(size_t ei
{hData
->mFds
[fi
].mEvStart
};ei
< hData
->mFds
[fi
].mEvs
.size();++ei
)
574 for(auto &azd
: hData
->mFds
[fi
].mEvs
[ei
].mAzs
)
576 for(size_t ti
{0};ti
< channels
;++ti
)
578 for(size_t i
{0};i
< m
;++i
)
579 azd
.mIrs
[ti
][i
] /= dfa
[(ti
* m
) + i
];
586 /* Given field and elevation indices and an azimuth, calculate the indices of
587 * the two HRIRs that bound the coordinate along with a factor for
588 * calculating the continuous HRIR using interpolation.
590 void CalcAzIndices(const HrirFdT
&field
, const uint ei
, const double az
, uint
*a0
, uint
*a1
, double *af
)
592 double f
{(2.0*al::numbers::pi
+ az
) * static_cast<double>(field
.mEvs
[ei
].mAzs
.size()) /
593 (2.0*al::numbers::pi
)};
594 const uint i
{static_cast<uint
>(f
) % static_cast<uint
>(field
.mEvs
[ei
].mAzs
.size())};
598 *a1
= (i
+ 1) % static_cast<uint
>(field
.mEvs
[ei
].mAzs
.size());
602 /* Synthesize any missing onset timings at the bottom elevations of each field.
603 * This just mirrors some top elevations for the bottom, and blends the
604 * remaining elevations (not an accurate model).
606 void SynthesizeOnsets(HrirDataT
*hData
)
608 const uint channels
{(hData
->mChannelType
== CT_STEREO
) ? 2u : 1u};
610 auto proc_field
= [channels
](HrirFdT
&field
) -> void
612 /* Get the starting elevation from the measurements, and use it as the
613 * upper elevation limit for what needs to be calculated.
615 const uint upperElevReal
{field
.mEvStart
};
616 if(upperElevReal
<= 0) return;
618 /* Get the lowest half of the missing elevations' delays by mirroring
619 * the top elevation delays. The responses are on a spherical grid
620 * centered between the ears, so these should align.
625 /* Take the polar opposite position of the desired measurement and
628 field
.mEvs
[0].mAzs
[0].mDelays
[0] = field
.mEvs
[field
.mEvs
.size()-1].mAzs
[0].mDelays
[1];
629 field
.mEvs
[0].mAzs
[0].mDelays
[1] = field
.mEvs
[field
.mEvs
.size()-1].mAzs
[0].mDelays
[0];
630 for(ei
= 1u;ei
< (upperElevReal
+1)/2;++ei
)
632 const uint topElev
{static_cast<uint
>(field
.mEvs
.size()-ei
-1)};
634 for(uint ai
{0u};ai
< field
.mEvs
[ei
].mAzs
.size();ai
++)
639 /* Rotate this current azimuth by a half-circle, and lookup
640 * the mirrored elevation to find the indices for the polar
641 * opposite position (may need blending).
643 const double az
{field
.mEvs
[ei
].mAzs
[ai
].mAzimuth
+ al::numbers::pi
};
644 CalcAzIndices(field
, topElev
, az
, &a0
, &a1
, &af
);
646 /* Blend the delays, and again, swap the ears. */
647 field
.mEvs
[ei
].mAzs
[ai
].mDelays
[0] = Lerp(
648 field
.mEvs
[topElev
].mAzs
[a0
].mDelays
[1],
649 field
.mEvs
[topElev
].mAzs
[a1
].mDelays
[1], af
);
650 field
.mEvs
[ei
].mAzs
[ai
].mDelays
[1] = Lerp(
651 field
.mEvs
[topElev
].mAzs
[a0
].mDelays
[0],
652 field
.mEvs
[topElev
].mAzs
[a1
].mDelays
[0], af
);
658 field
.mEvs
[0].mAzs
[0].mDelays
[0] = field
.mEvs
[field
.mEvs
.size()-1].mAzs
[0].mDelays
[0];
659 for(ei
= 1u;ei
< (upperElevReal
+1)/2;++ei
)
661 const uint topElev
{static_cast<uint
>(field
.mEvs
.size()-ei
-1)};
663 for(uint ai
{0u};ai
< field
.mEvs
[ei
].mAzs
.size();ai
++)
668 /* For mono data sets, mirror the azimuth front<->back
669 * since the other ear is a mirror of what we have (e.g.
670 * the left ear's back-left is simulated with the right
671 * ear's front-right, which uses the left ear's front-left
674 double az
{field
.mEvs
[ei
].mAzs
[ai
].mAzimuth
};
675 if(az
<= al::numbers::pi
) az
= al::numbers::pi
- az
;
676 else az
= (al::numbers::pi
*2.0)-az
+ al::numbers::pi
;
677 CalcAzIndices(field
, topElev
, az
, &a0
, &a1
, &af
);
679 field
.mEvs
[ei
].mAzs
[ai
].mDelays
[0] = Lerp(
680 field
.mEvs
[topElev
].mAzs
[a0
].mDelays
[0],
681 field
.mEvs
[topElev
].mAzs
[a1
].mDelays
[0], af
);
685 /* Record the lowest elevation filled in with the mirrored top. */
686 const uint lowerElevFake
{ei
-1u};
688 /* Fill in the remaining delays using bilinear interpolation. This
689 * helps smooth the transition back to the real delays.
691 for(;ei
< upperElevReal
;++ei
)
693 const double ef
{(field
.mEvs
[upperElevReal
].mElevation
- field
.mEvs
[ei
].mElevation
) /
694 (field
.mEvs
[upperElevReal
].mElevation
- field
.mEvs
[lowerElevFake
].mElevation
)};
696 for(uint ai
{0u};ai
< field
.mEvs
[ei
].mAzs
.size();ai
++)
701 double az
{field
.mEvs
[ei
].mAzs
[ai
].mAzimuth
};
702 CalcAzIndices(field
, upperElevReal
, az
, &a0
, &a1
, &af0
);
703 CalcAzIndices(field
, lowerElevFake
, az
, &a2
, &a3
, &af1
);
704 std::array
<double,4> blend
{{
705 (1.0-ef
) * (1.0-af0
),
711 for(uint ti
{0u};ti
< channels
;ti
++)
713 field
.mEvs
[ei
].mAzs
[ai
].mDelays
[ti
] =
714 field
.mEvs
[upperElevReal
].mAzs
[a0
].mDelays
[ti
]*blend
[0] +
715 field
.mEvs
[upperElevReal
].mAzs
[a1
].mDelays
[ti
]*blend
[1] +
716 field
.mEvs
[lowerElevFake
].mAzs
[a2
].mDelays
[ti
]*blend
[2] +
717 field
.mEvs
[lowerElevFake
].mAzs
[a3
].mDelays
[ti
]*blend
[3];
722 std::for_each(hData
->mFds
.begin(), hData
->mFds
.end(), proc_field
);
725 /* Attempt to synthesize any missing HRIRs at the bottom elevations of each
726 * field. Right now this just blends the lowest elevation HRIRs together and
727 * applies a low-pass filter to simulate body occlusion. It is a simple, if
730 void SynthesizeHrirs(HrirDataT
*hData
)
732 const uint channels
{(hData
->mChannelType
== CT_STEREO
) ? 2u : 1u};
733 auto htemp
= std::vector
<complex_d
>(hData
->mFftSize
);
734 const uint m
{hData
->mFftSize
/2u + 1u};
735 auto filter
= std::vector
<double>(m
);
736 const double beta
{3.5e-6 * hData
->mIrRate
};
738 auto proc_field
= [channels
,m
,beta
,&htemp
,&filter
](HrirFdT
&field
) -> void
740 const uint oi
{field
.mEvStart
};
743 for(uint ti
{0u};ti
< channels
;ti
++)
748 /* Use the lowest immediate-left response for the left ear and
749 * lowest immediate-right response for the right ear. Given no comb
750 * effects as a result of the left response reaching the right ear
751 * and vice-versa, this produces a decent phantom-center response
752 * underneath the head.
754 CalcAzIndices(field
, oi
, al::numbers::pi
/ ((ti
==0) ? -2.0 : 2.0), &a0
, &a1
, &af
);
755 for(uint i
{0u};i
< m
;i
++)
757 field
.mEvs
[0].mAzs
[0].mIrs
[ti
][i
] = Lerp(field
.mEvs
[oi
].mAzs
[a0
].mIrs
[ti
][i
],
758 field
.mEvs
[oi
].mAzs
[a1
].mIrs
[ti
][i
], af
);
762 for(uint ei
{1u};ei
< field
.mEvStart
;ei
++)
764 const double of
{static_cast<double>(ei
) / field
.mEvStart
};
765 const double b
{(1.0 - of
) * beta
};
766 std::array
<double,4> lp
{};
768 /* Calculate a low-pass filter to simulate body occlusion. */
769 lp
[0] = Lerp(1.0, lp
[0], b
);
770 lp
[1] = Lerp(lp
[0], lp
[1], b
);
771 lp
[2] = Lerp(lp
[1], lp
[2], b
);
772 lp
[3] = Lerp(lp
[2], lp
[3], b
);
774 for(size_t i
{1u};i
< htemp
.size();i
++)
776 lp
[0] = Lerp(0.0, lp
[0], b
);
777 lp
[1] = Lerp(lp
[0], lp
[1], b
);
778 lp
[2] = Lerp(lp
[1], lp
[2], b
);
779 lp
[3] = Lerp(lp
[2], lp
[3], b
);
782 /* Get the filter's frequency-domain response and extract the
783 * frequency magnitudes (phase will be reconstructed later)).
785 FftForward(static_cast<uint
>(htemp
.size()), htemp
.data());
786 std::transform(htemp
.cbegin(), htemp
.cbegin()+m
, filter
.begin(),
787 [](const complex_d c
) -> double { return std::abs(c
); });
789 for(uint ai
{0u};ai
< field
.mEvs
[ei
].mAzs
.size();ai
++)
794 CalcAzIndices(field
, oi
, field
.mEvs
[ei
].mAzs
[ai
].mAzimuth
, &a0
, &a1
, &af
);
795 for(uint ti
{0u};ti
< channels
;ti
++)
797 for(uint i
{0u};i
< m
;i
++)
799 /* Blend the two defined HRIRs closest to this azimuth,
800 * then blend that with the synthesized -90 elevation.
802 const double s1
{Lerp(field
.mEvs
[oi
].mAzs
[a0
].mIrs
[ti
][i
],
803 field
.mEvs
[oi
].mAzs
[a1
].mIrs
[ti
][i
], af
)};
804 const double s
{Lerp(field
.mEvs
[0].mAzs
[0].mIrs
[ti
][i
], s1
, of
)};
805 field
.mEvs
[ei
].mAzs
[ai
].mIrs
[ti
][i
] = s
* filter
[i
];
810 const double b
{beta
};
811 std::array
<double,4> lp
{};
812 lp
[0] = Lerp(1.0, lp
[0], b
);
813 lp
[1] = Lerp(lp
[0], lp
[1], b
);
814 lp
[2] = Lerp(lp
[1], lp
[2], b
);
815 lp
[3] = Lerp(lp
[2], lp
[3], b
);
817 for(size_t i
{1u};i
< htemp
.size();i
++)
819 lp
[0] = Lerp(0.0, lp
[0], b
);
820 lp
[1] = Lerp(lp
[0], lp
[1], b
);
821 lp
[2] = Lerp(lp
[1], lp
[2], b
);
822 lp
[3] = Lerp(lp
[2], lp
[3], b
);
825 FftForward(static_cast<uint
>(htemp
.size()), htemp
.data());
826 std::transform(htemp
.cbegin(), htemp
.cbegin()+m
, filter
.begin(),
827 [](const complex_d c
) -> double { return std::abs(c
); });
829 for(uint ti
{0u};ti
< channels
;ti
++)
831 for(uint i
{0u};i
< m
;i
++)
832 field
.mEvs
[0].mAzs
[0].mIrs
[ti
][i
] *= filter
[i
];
835 std::for_each(hData
->mFds
.begin(), hData
->mFds
.end(), proc_field
);
838 // The following routines assume a full set of HRIRs for all elevations.
840 /* Perform minimum-phase reconstruction using the magnitude responses of the
841 * HRIR set. Work is delegated to this struct, which runs asynchronously on one
842 * or more threads (sharing the same reconstructor object).
844 struct HrirReconstructor
{
845 std::vector
<al::span
<double>> mIrs
;
846 std::atomic
<size_t> mCurrent
{};
847 std::atomic
<size_t> mDone
{};
853 auto h
= std::vector
<complex_d
>(mFftSize
);
854 auto mags
= std::vector
<double>(mFftSize
);
855 size_t m
{(mFftSize
/2) + 1};
859 /* Load the current index to process. */
860 size_t idx
{mCurrent
.load()};
862 /* If the index is at the end, we're done. */
863 if(idx
>= mIrs
.size())
865 /* Otherwise, increment the current index atomically so other
866 * threads know to go to the next one. If this call fails, the
867 * current index was just changed by another thread and the new
868 * value is loaded into idx, which we'll recheck.
870 } while(!mCurrent
.compare_exchange_weak(idx
, idx
+1, std::memory_order_relaxed
));
872 /* Now do the reconstruction, and apply the inverse FFT to get the
873 * time-domain response.
875 for(size_t i
{0};i
< m
;++i
)
876 mags
[i
] = std::max(mIrs
[idx
][i
], Epsilon
);
877 MinimumPhase(mags
, h
);
878 FftInverse(mFftSize
, h
.data());
879 for(uint i
{0u};i
< mIrPoints
;++i
)
880 mIrs
[idx
][i
] = h
[i
].real();
882 /* Increment the number of IRs done. */
888 void ReconstructHrirs(const HrirDataT
*hData
, const uint numThreads
)
890 const uint channels
{(hData
->mChannelType
== CT_STEREO
) ? 2u : 1u};
892 /* Set up the reconstructor with the needed size info and pointers to the
895 HrirReconstructor reconstructor
;
896 reconstructor
.mCurrent
.store(0, std::memory_order_relaxed
);
897 reconstructor
.mDone
.store(0, std::memory_order_relaxed
);
898 reconstructor
.mFftSize
= hData
->mFftSize
;
899 reconstructor
.mIrPoints
= hData
->mIrPoints
;
900 for(const auto &field
: hData
->mFds
)
902 for(auto &elev
: field
.mEvs
)
904 for(const auto &azd
: elev
.mAzs
)
906 for(uint ti
{0u};ti
< channels
;ti
++)
907 reconstructor
.mIrs
.push_back(azd
.mIrs
[ti
]);
912 /* Launch threads to work on reconstruction. */
913 std::vector
<std::thread
> thrds
;
914 thrds
.reserve(numThreads
);
915 for(size_t i
{0};i
< numThreads
;++i
)
916 thrds
.emplace_back(std::mem_fn(&HrirReconstructor::Worker
), &reconstructor
);
918 /* Keep track of the number of IRs done, periodically reporting it. */
921 std::this_thread::sleep_for(std::chrono::milliseconds
{50});
923 count
= reconstructor
.mDone
.load();
924 size_t pcdone
{count
* 100 / reconstructor
.mIrs
.size()};
926 printf("\r%3zu%% done (%zu of %zu)", pcdone
, count
, reconstructor
.mIrs
.size());
928 } while(count
< reconstructor
.mIrs
.size());
931 for(auto &thrd
: thrds
)
938 // Normalize the HRIR set and slightly attenuate the result.
939 void NormalizeHrirs(HrirDataT
*hData
)
941 const uint channels
{(hData
->mChannelType
== CT_STEREO
) ? 2u : 1u};
942 const uint irSize
{hData
->mIrPoints
};
944 /* Find the maximum amplitude and RMS out of all the IRs. */
945 struct LevelPair
{ double amp
, rms
; };
946 auto mesasure_channel
= [irSize
](const LevelPair levels
, al::span
<const double> ir
)
948 /* Calculate the peak amplitude and RMS of this IR. */
949 ir
= ir
.first(irSize
);
950 auto current
= std::accumulate(ir
.cbegin(), ir
.cend(), LevelPair
{0.0, 0.0},
951 [](const LevelPair cur
, const double impulse
)
953 return LevelPair
{std::max(std::abs(impulse
), cur
.amp
), cur
.rms
+ impulse
*impulse
};
955 current
.rms
= std::sqrt(current
.rms
/ irSize
);
957 /* Accumulate levels by taking the maximum amplitude and RMS. */
958 return LevelPair
{std::max(current
.amp
, levels
.amp
), std::max(current
.rms
, levels
.rms
)};
960 auto measure_azi
= [channels
,mesasure_channel
](const LevelPair levels
, const HrirAzT
&azi
)
961 { return std::accumulate(azi
.mIrs
.begin(), azi
.mIrs
.begin()+channels
, levels
, mesasure_channel
); };
962 auto measure_elev
= [measure_azi
](const LevelPair levels
, const HrirEvT
&elev
)
963 { return std::accumulate(elev
.mAzs
.cbegin(), elev
.mAzs
.cend(), levels
, measure_azi
); };
964 auto measure_field
= [measure_elev
](const LevelPair levels
, const HrirFdT
&field
)
965 { return std::accumulate(field
.mEvs
.cbegin(), field
.mEvs
.cend(), levels
, measure_elev
); };
967 const auto maxlev
= std::accumulate(hData
->mFds
.begin(), hData
->mFds
.end(),
968 LevelPair
{0.0, 0.0}, measure_field
);
970 /* Normalize using the maximum RMS of the HRIRs. The RMS measure for the
971 * non-filtered signal is of an impulse with equal length (to the filter):
973 * rms_impulse = sqrt(sum([ 1^2, 0^2, 0^2, ... ]) / n)
976 * This helps keep a more consistent volume between the non-filtered signal
977 * and various data sets.
979 double factor
{std::sqrt(1.0 / irSize
) / maxlev
.rms
};
981 /* Also ensure the samples themselves won't clip. */
982 factor
= std::min(factor
, 0.99/maxlev
.amp
);
984 /* Now scale all IRs by the given factor. */
985 auto proc_channel
= [irSize
,factor
](al::span
<double> ir
)
987 ir
= ir
.first(irSize
);
988 std::transform(ir
.cbegin(), ir
.cend(), ir
.begin(),
989 [factor
](double s
) { return s
* factor
; });
991 auto proc_azi
= [channels
,proc_channel
](HrirAzT
&azi
)
992 { std::for_each(azi
.mIrs
.begin(), azi
.mIrs
.begin()+channels
, proc_channel
); };
993 auto proc_elev
= [proc_azi
](HrirEvT
&elev
)
994 { std::for_each(elev
.mAzs
.begin(), elev
.mAzs
.end(), proc_azi
); };
995 auto proc1_field
= [proc_elev
](HrirFdT
&field
)
996 { std::for_each(field
.mEvs
.begin(), field
.mEvs
.end(), proc_elev
); };
998 std::for_each(hData
->mFds
.begin(), hData
->mFds
.end(), proc1_field
);
1001 // Calculate the left-ear time delay using a spherical head model.
1002 double CalcLTD(const double ev
, const double az
, const double rad
, const double dist
)
1004 double azp
, dlp
, l
, al
;
1006 azp
= std::asin(std::cos(ev
) * std::sin(az
));
1007 dlp
= std::sqrt((dist
*dist
) + (rad
*rad
) + (2.0*dist
*rad
*sin(azp
)));
1008 l
= std::sqrt((dist
*dist
) - (rad
*rad
));
1009 al
= (0.5 * al::numbers::pi
) + azp
;
1011 dlp
= l
+ (rad
* (al
- std::acos(rad
/ dist
)));
1015 // Calculate the effective head-related time delays for each minimum-phase
1016 // HRIR. This is done per-field since distance delay is ignored.
1017 void CalculateHrtds(const HeadModelT model
, const double radius
, HrirDataT
*hData
)
1019 uint channels
= (hData
->mChannelType
== CT_STEREO
) ? 2 : 1;
1020 double customRatio
{radius
/ hData
->mRadius
};
1023 if(model
== HM_Sphere
)
1025 for(auto &field
: hData
->mFds
)
1027 for(auto &elev
: field
.mEvs
)
1029 for(auto &azd
: elev
.mAzs
)
1031 for(ti
= 0;ti
< channels
;ti
++)
1032 azd
.mDelays
[ti
] = CalcLTD(elev
.mElevation
, azd
.mAzimuth
, radius
, field
.mDistance
);
1037 else if(customRatio
!= 1.0)
1039 for(auto &field
: hData
->mFds
)
1041 for(auto &elev
: field
.mEvs
)
1043 for(auto &azd
: elev
.mAzs
)
1045 for(ti
= 0;ti
< channels
;ti
++)
1046 azd
.mDelays
[ti
] *= customRatio
;
1052 double maxHrtd
{0.0};
1053 for(auto &field
: hData
->mFds
)
1055 double minHrtd
{std::numeric_limits
<double>::infinity()};
1056 for(auto &elev
: field
.mEvs
)
1058 for(auto &azd
: elev
.mAzs
)
1060 for(ti
= 0;ti
< channels
;ti
++)
1061 minHrtd
= std::min(azd
.mDelays
[ti
], minHrtd
);
1065 for(auto &elev
: field
.mEvs
)
1067 for(auto &azd
: elev
.mAzs
)
1069 for(ti
= 0;ti
< channels
;ti
++)
1071 azd
.mDelays
[ti
] = (azd
.mDelays
[ti
]-minHrtd
) * hData
->mIrRate
;
1072 maxHrtd
= std::max(maxHrtd
, azd
.mDelays
[ti
]);
1077 if(maxHrtd
> MaxHrtd
)
1079 printf(" Scaling for max delay of %f samples to %f\n...\n", maxHrtd
, MaxHrtd
);
1080 const double scale
{MaxHrtd
/ maxHrtd
};
1081 for(auto &field
: hData
->mFds
)
1083 for(auto &elev
: field
.mEvs
)
1085 for(auto &azd
: elev
.mAzs
)
1087 for(ti
= 0;ti
< channels
;ti
++)
1088 azd
.mDelays
[ti
] *= scale
;
1097 // Allocate and configure dynamic HRIR structures.
1098 bool PrepareHrirData(const al::span
<const double> distances
,
1099 const al::span
<const uint
,MAX_FD_COUNT
> evCounts
,
1100 const al::span
<const std::array
<uint
,MAX_EV_COUNT
>,MAX_FD_COUNT
> azCounts
, HrirDataT
*hData
)
1102 uint evTotal
{0}, azTotal
{0};
1104 for(size_t fi
{0};fi
< distances
.size();++fi
)
1106 evTotal
+= evCounts
[fi
];
1107 for(size_t ei
{0};ei
< evCounts
[fi
];++ei
)
1108 azTotal
+= azCounts
[fi
][ei
];
1110 if(!evTotal
|| !azTotal
)
1113 hData
->mEvsBase
.resize(evTotal
);
1114 hData
->mAzsBase
.resize(azTotal
);
1115 hData
->mFds
.resize(distances
.size());
1116 hData
->mIrCount
= azTotal
;
1119 for(size_t fi
{0};fi
< distances
.size();++fi
)
1121 hData
->mFds
[fi
].mDistance
= distances
[fi
];
1122 hData
->mFds
[fi
].mEvStart
= 0;
1123 hData
->mFds
[fi
].mEvs
= al::span
{hData
->mEvsBase
}.subspan(evTotal
, evCounts
[fi
]);
1124 evTotal
+= evCounts
[fi
];
1125 for(uint ei
{0};ei
< evCounts
[fi
];++ei
)
1127 uint azCount
= azCounts
[fi
][ei
];
1129 hData
->mFds
[fi
].mEvs
[ei
].mElevation
= -al::numbers::pi
/ 2.0 + al::numbers::pi
* ei
/
1131 hData
->mFds
[fi
].mEvs
[ei
].mAzs
= al::span
{hData
->mAzsBase
}.subspan(azTotal
, azCount
);
1132 for(uint ai
{0};ai
< azCount
;ai
++)
1134 hData
->mFds
[fi
].mEvs
[ei
].mAzs
[ai
].mAzimuth
= 2.0 * al::numbers::pi
* ai
/ azCount
;
1135 hData
->mFds
[fi
].mEvs
[ei
].mAzs
[ai
].mIndex
= azTotal
+ ai
;
1136 hData
->mFds
[fi
].mEvs
[ei
].mAzs
[ai
].mDelays
[0] = 0.0;
1137 hData
->mFds
[fi
].mEvs
[ei
].mAzs
[ai
].mDelays
[1] = 0.0;
1138 hData
->mFds
[fi
].mEvs
[ei
].mAzs
[ai
].mIrs
[0] = {};
1139 hData
->mFds
[fi
].mEvs
[ei
].mAzs
[ai
].mIrs
[1] = {};
1150 /* Parse the data set definition and process the source data, storing the
1151 * resulting data set as desired. If the input name is NULL it will read
1152 * from standard input.
1154 bool ProcessDefinition(std::string_view inName
, const uint outRate
, const ChannelModeT chanMode
,
1155 const bool farfield
, const uint numThreads
, const uint fftSize
, const bool equalize
,
1156 const bool surface
, const double limit
, const uint truncSize
, const HeadModelT model
,
1157 const double radius
, const std::string_view outName
)
1161 printf("Using %u thread%s.\n", numThreads
, (numThreads
==1)?"":"s");
1162 if(inName
.empty() || inName
== "-"sv
)
1165 printf("Reading HRIR definition from %.*s...\n", al::sizei(inName
), inName
.data());
1166 if(!LoadDefInput(std::cin
, {}, inName
, fftSize
, truncSize
, outRate
, chanMode
, &hData
))
1171 auto input
= std::make_unique
<std::ifstream
>(std::filesystem::u8path(inName
));
1172 if(!input
->is_open())
1174 fprintf(stderr
, "Error: Could not open input file '%.*s'\n", al::sizei(inName
),
1179 std::array
<char,4> startbytes
{};
1180 input
->read(startbytes
.data(), startbytes
.size());
1181 if(input
->gcount() != startbytes
.size() || !input
->good())
1183 fprintf(stderr
, "Error: Could not read input file '%.*s'\n", al::sizei(inName
),
1188 if(startbytes
[0] == '\x89' && startbytes
[1] == 'H' && startbytes
[2] == 'D'
1189 && startbytes
[3] == 'F')
1192 printf("Reading HRTF data from %.*s...\n", al::sizei(inName
), inName
.data());
1193 if(!LoadSofaFile(inName
, numThreads
, fftSize
, truncSize
, outRate
, chanMode
, &hData
))
1198 printf("Reading HRIR definition from %.*s...\n", al::sizei(inName
), inName
.data());
1199 if(!LoadDefInput(*input
, startbytes
, inName
, fftSize
, truncSize
, outRate
, chanMode
,
1207 uint c
{(hData
.mChannelType
== CT_STEREO
) ? 2u : 1u};
1208 uint m
{hData
.mFftSize
/2u + 1u};
1209 auto dfa
= std::vector
<double>(size_t{c
} * m
);
1211 if(hData
.mFds
.size() > 1)
1213 fputs("Balancing field magnitudes...\n", stdout
);
1214 BalanceFieldMagnitudes(&hData
, c
, m
);
1216 fputs("Calculating diffuse-field average...\n", stdout
);
1217 CalculateDiffuseFieldAverage(&hData
, c
, m
, surface
, limit
, dfa
);
1218 fputs("Performing diffuse-field equalization...\n", stdout
);
1219 DiffuseFieldEqualize(c
, m
, dfa
, &hData
);
1221 if(hData
.mFds
.size() > 1)
1223 printf("Sorting %zu fields...\n", hData
.mFds
.size());
1224 std::sort(hData
.mFds
.begin(), hData
.mFds
.end(),
1225 [](const HrirFdT
&lhs
, const HrirFdT
&rhs
) noexcept
1226 { return lhs
.mDistance
< rhs
.mDistance
; });
1229 printf("Clearing %zu near field%s...\n", hData
.mFds
.size()-1,
1230 (hData
.mFds
.size()-1 != 1) ? "s" : "");
1231 hData
.mFds
.erase(hData
.mFds
.cbegin(), hData
.mFds
.cend()-1);
1234 fputs("Synthesizing missing elevations...\n", stdout
);
1235 if(model
== HM_Dataset
)
1236 SynthesizeOnsets(&hData
);
1237 SynthesizeHrirs(&hData
);
1238 fputs("Performing minimum phase reconstruction...\n", stdout
);
1239 ReconstructHrirs(&hData
, numThreads
);
1240 fputs("Truncating minimum-phase HRIRs...\n", stdout
);
1241 hData
.mIrPoints
= truncSize
;
1242 fputs("Normalizing final HRIRs...\n", stdout
);
1243 NormalizeHrirs(&hData
);
1244 fputs("Calculating impulse delays...\n", stdout
);
1245 CalculateHrtds(model
, (radius
> DefaultCustomRadius
) ? radius
: hData
.mRadius
, &hData
);
1247 const auto rateStr
= std::to_string(hData
.mIrRate
);
1248 const auto expName
= StrSubst(outName
, "%r"sv
, rateStr
);
1249 printf("Creating MHR data set %s...\n", expName
.c_str());
1250 return StoreMhr(&hData
, expName
);
1253 void PrintHelp(const std::string_view argv0
, FILE *ofile
)
1255 fprintf(ofile
, "Usage: %.*s [<option>...]\n\n", al::sizei(argv0
), argv0
.data());
1256 fprintf(ofile
, "Options:\n");
1257 fprintf(ofile
, " -r <rate> Change the data set sample rate to the specified value and\n");
1258 fprintf(ofile
, " resample the HRIRs accordingly.\n");
1259 fprintf(ofile
, " -m Change the data set to mono, mirroring the left ear for the\n");
1260 fprintf(ofile
, " right ear.\n");
1261 fprintf(ofile
, " -a Change the data set to single field, using the farthest field.\n");
1262 fprintf(ofile
, " -j <threads> Number of threads used to process HRIRs (default: 2).\n");
1263 fprintf(ofile
, " -f <points> Override the FFT window size (default: %u).\n", DefaultFftSize
);
1264 fprintf(ofile
, " -e {on|off} Toggle diffuse-field equalization (default: %s).\n", (DefaultEqualize
? "on" : "off"));
1265 fprintf(ofile
, " -s {on|off} Toggle surface-weighted diffuse-field average (default: %s).\n", (DefaultSurface
? "on" : "off"));
1266 fprintf(ofile
, " -l {<dB>|none} Specify a limit to the magnitude range of the diffuse-field\n");
1267 fprintf(ofile
, " average (default: %.2f).\n", DefaultLimit
);
1268 fprintf(ofile
, " -w <points> Specify the size of the truncation window that's applied\n");
1269 fprintf(ofile
, " after minimum-phase reconstruction (default: %u).\n", DefaultTruncSize
);
1270 fprintf(ofile
, " -d {dataset| Specify the model used for calculating the head-delay timing\n");
1271 fprintf(ofile
, " sphere} values (default: %s).\n", ((HM_Default
== HM_Dataset
) ? "dataset" : "sphere"));
1272 fprintf(ofile
, " -c <radius> Use a customized head radius measured to-ear in meters.\n");
1273 fprintf(ofile
, " -i <filename> Specify an HRIR definition file to use (defaults to stdin).\n");
1274 fprintf(ofile
, " -o <filename> Specify an output file. Use of '%%r' will be substituted with\n");
1275 fprintf(ofile
, " the data set sample rate.\n");
1278 // Standard command line dispatch.
1279 int main(al::span
<std::string_view
> args
)
1283 fputs("HRTF Processing and Composition Utility\n\n", stdout
);
1284 PrintHelp(args
[0], stdout
);
1288 std::string_view outName
{"./oalsoft_hrtf_%r.mhr"sv
};
1290 ChannelModeT chanMode
{CM_AllowStereo
};
1291 uint fftSize
{DefaultFftSize
};
1292 bool equalize
{DefaultEqualize
};
1293 bool surface
{DefaultSurface
};
1294 double limit
{DefaultLimit
};
1296 uint truncSize
{DefaultTruncSize
};
1297 HeadModelT model
{HM_Default
};
1298 double radius
{DefaultCustomRadius
};
1299 bool farfield
{false};
1300 std::string_view inName
;
1302 const std::string_view optlist
{"r:maj:f:e:s:l:w:d:c:e:i:o:h"sv
};
1303 const auto arg0
= args
[0];
1304 args
= args
.subspan(1);
1305 std::string_view optarg
;
1308 auto getarg
= [&args
,&argplace
,&optarg
,optlist
]
1310 while(!args
.empty() && argplace
>= args
[0].size())
1313 args
= args
.subspan(1);
1320 if(args
[0] == "--"sv
)
1323 if(args
[0][0] != '-' || args
[0].size() == 1)
1325 fprintf(stderr
, "Invalid argument: %.*s\n", al::sizei(args
[0]), args
[0].data());
1331 const char nextopt
{args
[0][argplace
]};
1332 const auto listidx
= optlist
.find(nextopt
);
1333 if(listidx
>= optlist
.size())
1335 fprintf(stderr
, "Unknown argument: -%c\n", nextopt
);
1338 const bool needsarg
{listidx
+1 < optlist
.size() && optlist
[listidx
+1] == ':'};
1339 if(needsarg
&& (argplace
+1 < args
[0].size() || args
.size() < 2))
1341 fprintf(stderr
, "Missing parameter for argument: -%c\n", nextopt
);
1344 if(++argplace
== args
[0].size())
1349 args
= args
.subspan(1u + needsarg
);
1352 return int{nextopt
};
1355 while(auto opt
= getarg())
1357 std::size_t endpos
{};
1361 outRate
= static_cast<uint
>(std::stoul(std::string
{optarg
}, &endpos
, 10));
1362 if(endpos
!= optarg
.size() || outRate
< MIN_RATE
|| outRate
> MAX_RATE
)
1364 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1365 al::sizei(optarg
), optarg
.data(), opt
, MIN_RATE
, MAX_RATE
);
1371 chanMode
= CM_ForceMono
;
1379 numThreads
= static_cast<uint
>(std::stoul(std::string
{optarg
}, &endpos
, 10));
1380 if(endpos
!= optarg
.size() || numThreads
> 64)
1382 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1383 al::sizei(optarg
), optarg
.data(), opt
, 0, 64);
1387 numThreads
= std::thread::hardware_concurrency();
1391 fftSize
= static_cast<uint
>(std::stoul(std::string
{optarg
}, &endpos
, 10));
1392 if(endpos
!= optarg
.size() || (fftSize
&(fftSize
-1)) || fftSize
< MinFftSize
1393 || fftSize
> MaxFftSize
)
1395 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected a power-of-two between %u to %u.\n",
1396 al::sizei(optarg
), optarg
.data(), opt
, MinFftSize
, MaxFftSize
);
1402 if(optarg
== "on"sv
)
1404 else if(optarg
== "off"sv
)
1408 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected on or off.\n",
1409 al::sizei(optarg
), optarg
.data(), opt
);
1415 if(optarg
== "on"sv
)
1417 else if(optarg
== "off"sv
)
1421 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected on or off.\n",
1422 al::sizei(optarg
), optarg
.data(), opt
);
1428 if(optarg
== "none"sv
)
1432 limit
= std::stod(std::string
{optarg
}, &endpos
);
1433 if(endpos
!= optarg
.size() || limit
< MinLimit
|| limit
> MaxLimit
)
1435 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %.0f to %.0f.\n",
1436 al::sizei(optarg
), optarg
.data(), opt
, MinLimit
, MaxLimit
);
1443 truncSize
= static_cast<uint
>(std::stoul(std::string
{optarg
}, &endpos
, 10));
1444 if(endpos
!= optarg
.size() || truncSize
< MinTruncSize
|| truncSize
> MaxTruncSize
)
1446 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %u to %u.\n",
1447 al::sizei(optarg
), optarg
.data(), opt
, MinTruncSize
, MaxTruncSize
);
1453 if(optarg
== "dataset"sv
)
1455 else if(optarg
== "sphere"sv
)
1459 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected dataset or sphere.\n",
1460 al::sizei(optarg
), optarg
.data(), opt
);
1466 radius
= std::stod(std::string
{optarg
}, &endpos
);
1467 if(endpos
!= optarg
.size() || radius
< MinCustomRadius
|| radius
> MaxCustomRadius
)
1469 fprintf(stderr
, "\nError: Got unexpected value \"%.*s\" for option -%c, expected between %.2f to %.2f.\n",
1470 al::sizei(optarg
), optarg
.data(), opt
, MinCustomRadius
, MaxCustomRadius
);
1484 PrintHelp(arg0
, stdout
);
1488 PrintHelp(arg0
, stderr
);
1493 const int ret
{ProcessDefinition(inName
, outRate
, chanMode
, farfield
, numThreads
, fftSize
,
1494 equalize
, surface
, limit
, truncSize
, model
, radius
, outName
)};
1496 fputs("Operation completed.\n", stdout
);
1498 return EXIT_SUCCESS
;
1503 int main(int argc
, char **argv
)
1506 auto args
= std::vector
<std::string_view
>(static_cast<unsigned int>(argc
));
1507 std::copy_n(argv
, args
.size(), args
.begin());
1508 return main(al::span
{args
});