-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
FEAT(client): Add HRTF-based spatialization #7085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
467e14d
27ce638
89f67ab
871ec71
a3d5f17
d42dddf
8de0906
d3a05e7
ede3832
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # data/hrtf/default.sofa — Provenance | ||
|
|
||
| ## Source | ||
|
|
||
| `default.sofa` is the CIPIC subject 124 HRTF, copied verbatim from the | ||
| **Valve Steam Audio** open-source SDK repository: | ||
|
|
||
| - Repository: https://github.com/ValveSoftware/steam-audio | ||
| - File path: `core/data/hrtf/cipic_124.sofa` | ||
| - Commit: `f88bd4e443ffdec9ec14ec52d2702de9702411a2` | ||
| ("Merge from Perforce 2024-02-19 14:28:05.862022") | ||
|
|
||
| No processing was applied by Steam Audio beyond what is in the original CIPIC | ||
| distribution — Steam Audio loads the file as-is using `mysofa_open_no_norm()`. | ||
|
|
||
| ## Original dataset | ||
|
|
||
| CIPIC subject 124 is from the CIPIC Interface Laboratory HRTF Database | ||
| (UC Davis, Center for Image Processing and Integrated Computing). | ||
|
|
||
| The original CIPIC website (https://www.ece.ucdavis.edu/cipic/spatial-sound/hrtf-data/) | ||
| is no longer reachable. Mirrors and references: | ||
|
|
||
| - GitHub mirror of the dataset: https://github.com/amini-allight/cipic-hrtf-database | ||
| - Web Archive snapshot of the original page: | ||
| https://web.archive.org/web/20170916053150/http://interface.cipic.ucdavis.edu/sound/hrtf.html | ||
|
|
||
| ## Why file choice matters | ||
|
|
||
| Raw HRTF measurements typically have strong spectral coloration — the frequency | ||
| response at front incidence may vary by 20 dB or more across the audible range. | ||
| When used directly for binaural rendering this colours speech noticeably (a | ||
| "telephone / band-pass" quality). A well-behaved HRTF for voice communication | ||
| should have a relatively flat front-incidence response (within ±10 dB from 100 Hz | ||
| to 8 kHz), with spectral shaping appearing primarily as pinna notches above 8 kHz | ||
| where it contributes to the spatial illusion rather than colouring speech. | ||
|
|
||
| The cipic_124.sofa file meets this criterion: its front-incidence response is flat | ||
| within ±5 dB from 100 Hz to 8 kHz. Whether this flatness is inherent to subject 124's | ||
| measurements or was applied as diffuse-field equalization upstream is not documented | ||
| by Steam Audio. | ||
|
|
||
| ## License | ||
|
|
||
| Copyright (c) 2001 The Regents of the University of California. All Rights Reserved. | ||
|
|
||
| The CIPIC database is made available for educational, research, and commercial use | ||
| with an acknowledgment request (see Steam Audio's `core/THIRDPARTY.md` for the full | ||
| license text). Steam Audio redistributes it under their Apache 2.0 SDK licence. | ||
|
|
||
| ## Updating | ||
|
|
||
| To replace this file with a different HRTF: | ||
|
|
||
| 1. Obtain a SOFA file in the `SimpleFreeFieldHRIR` convention (44100 or 48000 Hz) | ||
| 2. Prefer a file with a flat front-incidence response (diffuse-field equalized) | ||
| 3. Verify with `ffmpeg -i test_impulse.wav -af "sofalizer=sofa=<file>:type=freq:rotation=0:elevation=0" ir_0deg.wav` | ||
| and inspect the spectrum — it should be within ±10 dB from 100 Hz to 8 kHz at 0° |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |||||||||||||||||||||||||||||||||
| #include <chrono> | ||||||||||||||||||||||||||||||||||
| #include <cmath> | ||||||||||||||||||||||||||||||||||
| #include <memory> | ||||||||||||||||||||||||||||||||||
| #include <vector> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Remember that we cannot use static member classes that are not pointers, as the constructor | ||||||||||||||||||||||||||||||||||
| // for AudioOutputRegistrar() might be called before they are initialized, as the constructor | ||||||||||||||||||||||||||||||||||
|
|
@@ -227,9 +228,19 @@ void AudioOutput::removeBuffer(const void *buffer, bool acquireWriteLock) { | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (auto iter = qmOutputs.begin(); iter != qmOutputs.end(); ++iter) { | ||||||||||||||||||||||||||||||||||
| if (iter.value() == buffer) { | ||||||||||||||||||||||||||||||||||
| #ifdef USE_HRTF | ||||||||||||||||||||||||||||||||||
| // Capture the pointer value as source ID before deletion. | ||||||||||||||||||||||||||||||||||
| const auto hrtfSourceId = | ||||||||||||||||||||||||||||||||||
| static_cast< unsigned int >(reinterpret_cast< uintptr_t >(iter.value())); | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+231
to
+235
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's look at the specific file and lines mentioned
cd src/mumble && wc -l AudioOutput.cpp && head -n 240 AudioOutput.cpp | tail -n 20Repository: mumble-voip/mumble Length of output: 853 🏁 Script executed: # Search for HRTF-related definitions and usage patterns
rg -n "HrtfSpatializer|spatialize|removeSource|sourceId" --type-list | head -20Repository: mumble-voip/mumble Length of output: 451 🏁 Script executed: # Find files containing HrtfSpatializer
fd -e h -e hpp -e cpp | xargs rg -l "HrtfSpatializer" 2>/dev/nullRepository: mumble-voip/mumble Length of output: 227 🏁 Script executed: # Examine HrtfSpatializer header to see class definition and method signatures
cat -n src/mumble/HrtfSpatializer.hRepository: mumble-voip/mumble Length of output: 4748 🏁 Script executed: # Check the removeSource method and class structure
rg -A 10 -B 2 "removeSource|spatialize" src/mumble/HrtfSpatializer.hRepository: mumble-voip/mumble Length of output: 1323 🏁 Script executed: # Check HrtfSpatializer implementation
head -n 100 src/mumble/HrtfSpatializer.cppRepository: mumble-voip/mumble Length of output: 3103 🏁 Script executed: # Search for the map/container that stores HRTFs
rg -n "map|Map|unordered_map" src/mumble/HrtfSpatializer.h src/mumble/HrtfSpatializer.cppRepository: mumble-voip/mumble Length of output: 230 🏁 Script executed: # Look at broader context in AudioOutput.cpp around the problematic code
sed -n '220,250p' src/mumble/AudioOutput.cppRepository: mumble-voip/mumble Length of output: 1048 🏁 Script executed: # Search for all removeSource calls in AudioOutput.cpp
rg -n "removeSource" src/mumble/AudioOutput.cppRepository: mumble-voip/mumble Length of output: 116 🏁 Script executed: # Search for spatialize calls in AudioOutput.cpp
rg -n "spatialize" src/mumble/AudioOutput.cppRepository: mumble-voip/mumble Length of output: 143 🏁 Script executed: # Get full context around spatialize calls
rg -B 5 -A 5 "spatialize" src/mumble/AudioOutput.cppRepository: mumble-voip/mumble Length of output: 622 🏁 Script executed: # Check if there are any other places where sourceId is created from a pointer
rg -n "hrtfSourceId\|sourceId.*reinterpret\|uintptr_t" src/mumble/AudioOutput.cpp | head -20Repository: mumble-voip/mumble Length of output: 44 🏁 Script executed: # Look for the broader context where spatialize is called to see how sourceId is derived
sed -n '780,820p' src/mumble/AudioOutput.cppRepository: mumble-voip/mumble Length of output: 2115 Use 64‑bit IDs throughout the HRTF spatializer to prevent pointer truncation collisions. Casting buffer pointers to Update both call sites (lines 785–786 for
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| delete iter.value(); | ||||||||||||||||||||||||||||||||||
| qmOutputs.erase(iter); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| #ifdef USE_HRTF | ||||||||||||||||||||||||||||||||||
| if (m_hrtfSpatializer) { | ||||||||||||||||||||||||||||||||||
| m_hrtfSpatializer->removeSource(hrtfSourceId); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+239
to
+242
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's examine the AudioOutput.cpp file around lines 239-242
fd -e cpp -e h | grep -i "AudioOutput" | head -20Repository: mumble-voip/mumble Length of output: 397 🏁 Script executed: # Run the provided script to locate removeBuffer/removeUser call sites
rg -n "removeBuffer\(|removeUser\(" --type cppRepository: mumble-voip/mumble Length of output: 2132 🏁 Script executed: # Check for HrtfSpatializer class definition and documentation
rg -n "class HrtfSpatializer" --type cpp --type hRepository: mumble-voip/mumble Length of output: 117 🏁 Script executed: # Look for the removeSource method definition in HrtfSpatializer
rg -n "removeSource" --type cpp --type hRepository: mumble-voip/mumble Length of output: 402 🏁 Script executed: # Examine AudioOutput.cpp around lines 239-242
sed -n '220,260p' src/mumble/AudioOutput.cppRepository: mumble-voip/mumble Length of output: 1290 🏁 Script executed: # Look at the removeBuffer and removeUser methods to understand thread context
sed -n '217,295p' src/mumble/AudioOutput.cppRepository: mumble-voip/mumble Length of output: 2248 🏁 Script executed: # Check HrtfSpatializer.h for thread documentation
cat src/mumble/HrtfSpatializer.hRepository: mumble-voip/mumble Length of output: 3999 🏁 Script executed: # Check HrtfSpatializer implementation around removeSource
sed -n '210,230p' src/mumble/HrtfSpatializer.cppRepository: mumble-voip/mumble Length of output: 245 Defer HRTF source removal to the audio mixing thread.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -434,6 +445,12 @@ void AudioOutput::initializeMixer(const unsigned int *chanmasks, bool forceheadp | |||||||||||||||||||||||||||||||||
| static_cast< unsigned int >(iChannels * ((eSampleFormat == SampleFloat) ? sizeof(float) : sizeof(short))); | ||||||||||||||||||||||||||||||||||
| qWarning("AudioOutput: Initialized %d channel %d hz mixer", iChannels, iMixerFreq); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| #ifdef USE_HRTF | ||||||||||||||||||||||||||||||||||
| m_hrtfSpatializer = std::make_unique< HrtfSpatializer >(static_cast< int >(iMixerFreq), | ||||||||||||||||||||||||||||||||||
| static_cast< int >(iFrameSize)); | ||||||||||||||||||||||||||||||||||
| m_hrtfSpatializer->loadHRTF(Global::get().s.qsHrtfFile); | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (Global::get().s.bPositionalAudio && iChannels == 1) { | ||||||||||||||||||||||||||||||||||
| Log::logOrDefer(Log::Warning, tr("Positional audio cannot work with mono output devices!")); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -517,6 +534,14 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| bool validListener = false; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| #ifdef USE_HRTF | ||||||||||||||||||||||||||||||||||
| // Listener orientation vectors for HRTF direction computation. | ||||||||||||||||||||||||||||||||||
| // Set when positional audio is active (validListener == true). | ||||||||||||||||||||||||||||||||||
| Vector3D hrtfCameraDir = { 0.0f, 0.0f, 1.0f }; | ||||||||||||||||||||||||||||||||||
| Vector3D hrtfCameraAxis = { 0.0f, 1.0f, 0.0f }; | ||||||||||||||||||||||||||||||||||
| Vector3D hrtfRight = { 1.0f, 0.0f, 0.0f }; | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Initialize recorder if recording is enabled | ||||||||||||||||||||||||||||||||||
| std::shared_ptr< float[] > recbuff; | ||||||||||||||||||||||||||||||||||
| if (recorder) { | ||||||||||||||||||||||||||||||||||
|
|
@@ -580,6 +605,12 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { | |||||||||||||||||||||||||||||||||
| // Calculate right vector as front X top | ||||||||||||||||||||||||||||||||||
| Vector3D right = cameraAxis.crossProduct(cameraDir); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| #ifdef USE_HRTF | ||||||||||||||||||||||||||||||||||
| hrtfCameraDir = cameraDir; | ||||||||||||||||||||||||||||||||||
| hrtfCameraAxis = cameraAxis; | ||||||||||||||||||||||||||||||||||
| hrtfRight = right; | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||
| qWarning("Front: %f %f %f", front[0], front[1], front[2]); | ||||||||||||||||||||||||||||||||||
| qWarning("Top: %f %f %f", top[0], top[1], top[2]); | ||||||||||||||||||||||||||||||||||
|
|
@@ -736,6 +767,68 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { | |||||||||||||||||||||||||||||||||
| qWarning("Voice pos: %f %f %f", aop->fPos[0], aop->fPos[1], aop->fPos[2]); | ||||||||||||||||||||||||||||||||||
| qWarning("Voice dir: %f %f %f", connectionVec.x, connectionVec.y, connectionVec.z); | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| #ifdef USE_HRTF | ||||||||||||||||||||||||||||||||||
| if (nchan == 2 && m_hrtfSpatializer && m_hrtfSpatializer->isLoaded() | ||||||||||||||||||||||||||||||||||
| && Global::get().s.bHrtf) { | ||||||||||||||||||||||||||||||||||
| // HRTF binaural path: replaces the per-channel gain + ITD loop below. | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Compute source direction in listener-local frame (+X=right, +Y=up, +Z=forward). | ||||||||||||||||||||||||||||||||||
| const float localX = connectionVec.x * hrtfRight.x + connectionVec.y * hrtfRight.y | ||||||||||||||||||||||||||||||||||
| + connectionVec.z * hrtfRight.z; | ||||||||||||||||||||||||||||||||||
| const float localY = connectionVec.x * hrtfCameraAxis.x | ||||||||||||||||||||||||||||||||||
| + connectionVec.y * hrtfCameraAxis.y | ||||||||||||||||||||||||||||||||||
| + connectionVec.z * hrtfCameraAxis.z; | ||||||||||||||||||||||||||||||||||
| const float localZ = connectionVec.x * hrtfCameraDir.x | ||||||||||||||||||||||||||||||||||
| + connectionVec.y * hrtfCameraDir.y | ||||||||||||||||||||||||||||||||||
| + connectionVec.z * hrtfCameraDir.z; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Buffer pointer as source ID: stable lifetime, unique across speech + samples. | ||||||||||||||||||||||||||||||||||
| const auto sourceId = | ||||||||||||||||||||||||||||||||||
| static_cast< unsigned int >(reinterpret_cast< uintptr_t >(buffer)); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Downmix stereo to mono before spatialisation. | ||||||||||||||||||||||||||||||||||
| static thread_local std::vector< float > monoMix; | ||||||||||||||||||||||||||||||||||
| monoMix.resize(frameCount); | ||||||||||||||||||||||||||||||||||
| if (speech && speech->bStereo) { | ||||||||||||||||||||||||||||||||||
| for (unsigned int i = 0; i < frameCount; ++i) | ||||||||||||||||||||||||||||||||||
| monoMix[i] = (pfBuffer[2 * i] + pfBuffer[2 * i + 1]) * 0.5f; | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| for (unsigned int i = 0; i < frameCount; ++i) | ||||||||||||||||||||||||||||||||||
| monoMix[i] = pfBuffer[i]; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Spatialize: mono → interleaved binaural stereo (L,R,L,R,...). | ||||||||||||||||||||||||||||||||||
| static thread_local std::vector< float > hrtfOut; | ||||||||||||||||||||||||||||||||||
| hrtfOut.resize(frameCount * 2); | ||||||||||||||||||||||||||||||||||
| m_hrtfSpatializer->spatialize(sourceId, monoMix.data(), hrtfOut.data(), | ||||||||||||||||||||||||||||||||||
| frameCount, localX, localY, localZ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Apply distance attenuation only (dot=1.0 → pure distance falloff; | ||||||||||||||||||||||||||||||||||
| // the HRTF IR encodes ILD/ITD directional cues, no per-channel weighting needed). | ||||||||||||||||||||||||||||||||||
| const bool isAudible = (Global::get().s.fAudioMaxDistVolume > 0) | ||||||||||||||||||||||||||||||||||
| || (len < Global::get().s.fAudioMaxDistance); | ||||||||||||||||||||||||||||||||||
| const float gain = isAudible ? mul * calcGain(1.0f, len) * volumeAdjustment : 0.0f; | ||||||||||||||||||||||||||||||||||
| maxVolume = gain; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Ramp gain linearly across the block to avoid clicks on distance changes | ||||||||||||||||||||||||||||||||||
| // (mirrors per-sample interpolation in the non-HRTF path below). | ||||||||||||||||||||||||||||||||||
| // pfVolume[0] caches the previous block's gain; -1.0 signals first call. | ||||||||||||||||||||||||||||||||||
| if (!buffer->pfVolume) { | ||||||||||||||||||||||||||||||||||
| buffer->pfVolume = new float[nchan]; | ||||||||||||||||||||||||||||||||||
| buffer->pfVolume[0] = -1.0f; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| const float oldGain = (buffer->pfVolume[0] >= 0.0f) ? buffer->pfVolume[0] : gain; | ||||||||||||||||||||||||||||||||||
| buffer->pfVolume[0] = gain; | ||||||||||||||||||||||||||||||||||
| const float gainInc = (gain - oldGain) / static_cast< float >(frameCount); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+816
to
+823
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initialize all pfVolume channels in the HRTF path. If HRTF is later disabled for an active buffer, the non‑HRTF path can read uninitialized 🛠️ Proposed fix- if (!buffer->pfVolume) {
- buffer->pfVolume = new float[nchan];
- buffer->pfVolume[0] = -1.0f;
- }
+ if (!buffer->pfVolume) {
+ buffer->pfVolume = new float[nchan];
+ for (unsigned int s = 0; s < nchan; ++s) {
+ buffer->pfVolume[s] = -1.0f;
+ }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| for (unsigned int i = 0; i < frameCount; ++i) { | ||||||||||||||||||||||||||||||||||
| const float g = oldGain + gainInc * static_cast< float >(i); | ||||||||||||||||||||||||||||||||||
| output[i * nchan + 0] += hrtfOut[2 * i] * g; | ||||||||||||||||||||||||||||||||||
| output[i * nchan + 1] += hrtfOut[2 * i + 1] * g; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| // Non-HRTF per-channel gain + ITD path (use `git diff -w` to review separately from indentation): | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
| if (!buffer->pfVolume) { | ||||||||||||||||||||||||||||||||||
| buffer->pfVolume = new float[nchan]; | ||||||||||||||||||||||||||||||||||
| for (unsigned int s = 0; s < nchan; ++s) | ||||||||||||||||||||||||||||||||||
|
|
@@ -812,6 +905,9 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| #ifdef USE_HRTF | ||||||||||||||||||||||||||||||||||
| } // end else: non-HRTF per-channel gain + ITD path | ||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| // Mix the current audio source into the output by adding it to the elements of the output buffer | ||||||||||||||||||||||||||||||||||
| // after having applied a volume adjustment | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -709,6 +709,46 @@ | |
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="9" column="1" colspan="3"> | ||
| <widget class="QCheckBox" name="qcbHrtf"> | ||
| <property name="toolTip"> | ||
| <string>Enable Head-Related Transfer Function (HRTF) binaural audio processing for headphones</string> | ||
| </property> | ||
| <property name="whatsThis"> | ||
| <string>When enabled, HRTF processing uses a measured head-related transfer function to render spatial audio binaurally. This provides improved elevation perception and front/back disambiguation compared to standard panning. Requires headphones for best effect.</string> | ||
| </property> | ||
| <property name="text"> | ||
| <string>Use HRTF binaural audio (headphones recommended)</string> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="10" column="1"> | ||
| <widget class="QLabel" name="qlHrtfFile"> | ||
| <property name="text"> | ||
| <string>HRTF File</string> | ||
| </property> | ||
| <property name="buddy"> | ||
| <cstring>qleHrtfFile</cstring> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="10" column="2"> | ||
| <widget class="QLineEdit" name="qleHrtfFile"> | ||
| <property name="toolTip"> | ||
| <string>Path to a custom SOFA file. Leave empty to use the default HRTF.</string> | ||
| </property> | ||
| <property name="placeholderText"> | ||
| <string>Default HRTF</string> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
| <item row="10" column="3"> | ||
| <widget class="QPushButton" name="qpbHrtfBrowse"> | ||
| <property name="text"> | ||
| <string>Browse...</string> | ||
| </property> | ||
| </widget> | ||
| </item> | ||
|
Comment on lines
+712
to
+751
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add the new HRTF controls to the tab order. The UI defines an explicit tab order, but the new HRTF widgets aren’t included, so keyboard navigation will skip them. ♿ Suggested tab order update <tabstop>qsBloom</tabstop>
<tabstop>qsbBloom</tabstop>
+ <tabstop>qcbHrtf</tabstop>
+ <tabstop>qleHrtfFile</tabstop>
+ <tabstop>qpbHrtfBrowse</tabstop>
<tabstop>qcbLoopback</tabstop>🤖 Prompt for AI Agents |
||
| </layout> | ||
| </widget> | ||
| </item> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid overwriting HRTF settings when the feature is compiled out.
With USE_HRTF disabled, the hidden controls will still save default/empty values and wipe a user’s previous HRTF settings. Consider preserving the existing values when HRTF is not available.
🔧 Suggested guard to preserve existing values
🤖 Prompt for AI Agents