Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@
[submodule "3rdparty/CLI11"]
path = 3rdparty/CLI11
url = https://github.com/CLIUtils/CLI11.git
[submodule "3rdparty/fftconvolver"]
path = 3rdparty/fftconvolver
url = https://github.com/HiFi-LoFi/FFTConvolver.git
1 change: 1 addition & 0 deletions 3rdparty/fftconvolver
Submodule fftconvolver added at f2cdeb
58 changes: 58 additions & 0 deletions data/hrtf/README.md
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°
Binary file added data/hrtf/default.sofa
Binary file not shown.
21 changes: 21 additions & 0 deletions src/mumble/AudioConfigDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "Utils.h"
#include "Global.h"

#include <QFileDialog>
#include <QSignalBlocker>

#include <cstdint>
Expand Down Expand Up @@ -682,6 +683,13 @@ AudioOutputDialog::AudioOutputDialog(Settings &st) : ConfigWidget(st) {
qlBloom->setToolTip(bloomTooltip);
qsBloom->setToolTip(bloomTooltip);
qsbBloom->setToolTip(bloomTooltip);

#ifndef USE_HRTF
qcbHrtf->setVisible(false);
qlHrtfFile->setVisible(false);
qleHrtfFile->setVisible(false);
qpbHrtfBrowse->setVisible(false);
#endif
}

QString AudioOutputDialog::title() const {
Expand Down Expand Up @@ -749,6 +757,8 @@ void AudioOutputDialog::load(const Settings &r) {
qsbBloom->setValue(static_cast< int >(r.fAudioBloom * 100));
loadCheckBox(qcbHeadphones, r.bPositionalHeadphone);
loadCheckBox(qcbPositional, r.bPositionalAudio);
loadCheckBox(qcbHrtf, r.bHrtf);
qleHrtfFile->setText(r.qsHrtfFile);

qsOtherVolume->setEnabled(r.bAttenuateOthersOnTalk || r.bAttenuateOthers);
qlOtherVolume->setEnabled(r.bAttenuateOthersOnTalk || r.bAttenuateOthers);
Expand Down Expand Up @@ -778,6 +788,8 @@ void AudioOutputDialog::save() const {
s.bPositionalAudio = qcbPositional->isChecked();
s.bPositionalHeadphone = qcbHeadphones->isChecked();
s.bExclusiveOutput = qcbExclusive->isChecked();
s.bHrtf = qcbHrtf->isChecked();
s.qsHrtfFile = qleHrtfFile->text();
Comment on lines +791 to +792
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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
-	s.bHrtf                          = qcbHrtf->isChecked();
-	s.qsHrtfFile                     = qleHrtfFile->text();
+#ifdef USE_HRTF
+	s.bHrtf                          = qcbHrtf->isChecked();
+	s.qsHrtfFile                     = qleHrtfFile->text();
+#endif
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/AudioConfigDialog.cpp` around lines 791 - 792, The assignments to
s.bHrtf and s.qsHrtfFile are overwriting stored HRTF settings even when HRTF
support is compiled out; wrap the two lines that read qcbHrtf->isChecked() and
qleHrtfFile->text() in a compile-time guard so they only run when USE_HRTF is
defined (e.g. `#ifdef` USE_HRTF ... `#endif`), leaving s.bHrtf and s.qsHrtfFile
untouched when the feature is disabled to preserve existing values; reference
the symbols s.bHrtf, s.qsHrtfFile, qcbHrtf, qleHrtfFile and the USE_HRTF macro
when making the change.



if (AudioOutputRegistrar::qmNew) {
Expand Down Expand Up @@ -959,3 +971,12 @@ void AudioOutputDialog::on_qcbAttenuateOthers_clicked(bool checked) {
void AudioOutputDialog::on_qcbOnlyAttenuateSameOutput_clicked(bool checked) {
qcbAttenuateLoopbacks->setEnabled(checked);
}

void AudioOutputDialog::on_qpbHrtfBrowse_clicked() {
const QString path = QFileDialog::getOpenFileName(
this, tr("Select HRTF SOFA file"), qleHrtfFile->text(),
tr("SOFA files (*.sofa);;All files (*)"));
if (!path.isEmpty()) {
qleHrtfFile->setText(path);
}
}
1 change: 1 addition & 0 deletions src/mumble/AudioConfigDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public slots:
void on_qcbAttenuateOthersOnTalk_clicked(bool checked);
void on_qcbAttenuateOthers_clicked(bool checked);
void on_qcbOnlyAttenuateSameOutput_clicked(bool checked);
void on_qpbHrtfBrowse_clicked();
};

#endif
96 changes: 96 additions & 0 deletions src/mumble/AudioOutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 20

Repository: 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 -20

Repository: 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/null

Repository: mumble-voip/mumble

Length of output: 227


🏁 Script executed:

# Examine HrtfSpatializer header to see class definition and method signatures
cat -n src/mumble/HrtfSpatializer.h

Repository: 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.h

Repository: mumble-voip/mumble

Length of output: 1323


🏁 Script executed:

# Check HrtfSpatializer implementation
head -n 100 src/mumble/HrtfSpatializer.cpp

Repository: 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.cpp

Repository: 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.cpp

Repository: mumble-voip/mumble

Length of output: 1048


🏁 Script executed:

# Search for all removeSource calls in AudioOutput.cpp
rg -n "removeSource" src/mumble/AudioOutput.cpp

Repository: mumble-voip/mumble

Length of output: 116


🏁 Script executed:

# Search for spatialize calls in AudioOutput.cpp
rg -n "spatialize" src/mumble/AudioOutput.cpp

Repository: mumble-voip/mumble

Length of output: 143


🏁 Script executed:

# Get full context around spatialize calls
rg -B 5 -A 5 "spatialize" src/mumble/AudioOutput.cpp

Repository: 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 -20

Repository: 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.cpp

Repository: mumble-voip/mumble

Length of output: 2115


Use 64‑bit IDs throughout the HRTF spatializer to prevent pointer truncation collisions.

Casting buffer pointers to unsigned int truncates their upper bits on 64‑bit systems. Two different buffers may collide in the lower 32 bits, causing the same HRTF state to be reused across different sources and corrupting spatialization.

Update both call sites (lines 785–786 for spatialize() and 232–234 for removeSource()) and the HrtfSpatializer class to use std::uintptr_t throughout:

  • spatialize(std::uintptr_t sourceId, ...)
  • removeSource(std::uintptr_t sourceId)
  • std::unordered_map< std::uintptr_t, std::unique_ptr< SourceState > > m_sources
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/AudioOutput.cpp` around lines 231 - 235, The HRTF spatializer uses
32-bit unsigned IDs causing pointer truncation on 64-bit systems; change
HrtfSpatializer to use std::uintptr_t for IDs by updating the class signature
(spatialize and removeSource to accept std::uintptr_t) and the member map type
to std::unordered_map<std::uintptr_t, std::unique_ptr<SourceState>> m_sources,
then update both call sites that currently cast buffer pointers to unsigned int
to instead cast via reinterpret_cast<std::uintptr_t>(ptr) and store/pass that
value (e.g., the hrtfSourceId capture in AudioOutput.cpp and the spatialize()
invocation), and add the proper include for std::uintptr_t (<cstdint> or
<cstddef>).

delete iter.value();
qmOutputs.erase(iter);

#ifdef USE_HRTF
if (m_hrtfSpatializer) {
m_hrtfSpatializer->removeSource(hrtfSourceId);
}
Comment on lines +239 to +242
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -20

Repository: mumble-voip/mumble

Length of output: 397


🏁 Script executed:

# Run the provided script to locate removeBuffer/removeUser call sites
rg -n "removeBuffer\(|removeUser\(" --type cpp

Repository: mumble-voip/mumble

Length of output: 2132


🏁 Script executed:

# Check for HrtfSpatializer class definition and documentation
rg -n "class HrtfSpatializer" --type cpp --type h

Repository: mumble-voip/mumble

Length of output: 117


🏁 Script executed:

# Look for the removeSource method definition in HrtfSpatializer
rg -n "removeSource" --type cpp --type h

Repository: mumble-voip/mumble

Length of output: 402


🏁 Script executed:

# Examine AudioOutput.cpp around lines 239-242
sed -n '220,260p' src/mumble/AudioOutput.cpp

Repository: 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.cpp

Repository: mumble-voip/mumble

Length of output: 2248


🏁 Script executed:

# Check HrtfSpatializer.h for thread documentation
cat src/mumble/HrtfSpatializer.h

Repository: mumble-voip/mumble

Length of output: 3999


🏁 Script executed:

# Check HrtfSpatializer implementation around removeSource
sed -n '210,230p' src/mumble/HrtfSpatializer.cpp

Repository: mumble-voip/mumble

Length of output: 245


Defer HRTF source removal to the audio mixing thread.

HrtfSpatializer documentation explicitly states all methods must run on the audio mixing thread. However, removeBuffer() can be called from non-audio contexts (via the bufferInvalidated signal or from removeUser()), causing removeSource() at line 241 to violate thread affinity. Queue the removal for execution within the mix loop instead of calling it directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/AudioOutput.cpp` around lines 239 - 242, removeSource() is being
called on HrtfSpatializer from non-audio threads (removeBuffer() via
bufferInvalidated or removeUser()), violating HrtfSpatializer's thread-affinity
rule; instead enqueue the removal to run on the audio mixing thread. Modify the
code path that currently calls m_hrtfSpatializer->removeSource(hrtfSourceId) to
push a task/command (e.g., a lambda or id) into the audio mixer's task/command
queue or a thread-safe pending-removals list that the mix loop processes, and in
the mix loop execute HrtfSpatializer::removeSource(hrtfSourceId) there; ensure
the enqueueing occurs where removeBuffer()/removeUser() can reach and that the
mix thread drains and performs the actual removeSource call.

#endif
break;
}
}
Expand Down Expand Up @@ -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!"));
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Initialize all pfVolume channels in the HRTF path.

If HRTF is later disabled for an active buffer, the non‑HRTF path can read uninitialized pfVolume[1], which can spike gain ramps. Initialize all channels to -1.0f.

🛠️ 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
if (!buffer->pfVolume) {
buffer->pfVolume = new float[nchan];
for (unsigned int s = 0; s < nchan; ++s) {
buffer->pfVolume[s] = -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);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/AudioOutput.cpp` around lines 816 - 823, The allocation for
buffer->pfVolume in AudioOutput.cpp only initializes channel 0, leaving
pfVolume[1..nchan-1] uninitialized; modify the pfVolume allocation branch (the
code that does buffer->pfVolume = new float[nchan]) to initialize all elements
to -1.0f (e.g., loop i from 0 to nchan and set buffer->pfVolume[i] = -1.0f)
before using pfVolume[0] and applying gain so that all channels are safe if HRTF
is later disabled.

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)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/mumble/AudioOutput.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
# include "ManualPlugin.h"
#endif

#ifdef USE_HRTF
# include "HrtfSpatializer.h"
#endif

#include <memory>

#ifndef SPEAKER_FRONT_LEFT
Expand Down Expand Up @@ -101,6 +105,10 @@ private slots:
QHash< unsigned int, Position2D > positions;
#endif

#ifdef USE_HRTF
std::unique_ptr< HrtfSpatializer > m_hrtfSpatializer;
#endif

void initializeMixer(const unsigned int *chanmasks, bool forceheadphone = false);
bool mix(void *output, unsigned int frameCount);

Expand Down
40 changes: 40 additions & 0 deletions src/mumble/AudioOutput.ui
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
Verify each finding against the current code and only fix it if needed.

In `@src/mumble/AudioOutput.ui` around lines 712 - 751, The tab order is missing
the new HRTF widgets, so add them into the existing tab sequence (e.g., in
setupUi or the .ui taborder section) by inserting setTabOrder calls (or the
equivalent UI XML <tabstops>) to include qcbHrtf, qlHrtfFile (if focusable) /
qleHrtfFile, and qpbHrtfBrowse in the correct logical position; ensure qcbHrtf
comes before qleHrtfFile and qpbHrtfBrowse (or match surrounding controls) so
keyboard navigation visits qcbHrtf → qleHrtfFile → qpbHrtfBrowse in the expected
order.

</layout>
</widget>
</item>
Expand Down
Loading
Loading