Skip to content
Score2 edited this page Aug 10, 2026 · 1 revision

Volume

AudioHub keeps loudness consistent across two machines. It deliberately does not keep the slider numbers consistent.

That is not a shortcut. The two cannot both be true, and this page contains the proof.


The problem, measured

Same slider position, two machines, real hardware on both ends. Measured on macOS built-in speakers versus a Windows output endpoint:

Slider macOS Windows Difference Perceived
0 % −63.5 dB −96.0 dB −32.5 dB quieter
10 % −43.4 dB −34.8 dB +8.7 dB ~1.8× louder
25 % −31.8 dB −21.0 dB +10.8 dB ~2.1× louder
50 % −18.6 dB −10.5 dB +8.1 dB ~1.8× louder
75 % −8.5 dB −4.4 dB +4.1 dB ~1.3× louder
90 % −3.3 dB −1.6 dB +1.7 dB ~1.1× louder
100 % 0 dB 0 dB 0 dB equal

Only 0 % and 100 % line up. Everything between them is wrong, and the worst error falls in the 10–50 % range people actually use — where Windows is roughly twice as loud as macOS at the same slider position.

There is no such thing as "the macOS curve"

A tempting framing is macOS uses one taper, Windows uses another. The measurements do not support it. Four different curves were found, three of them on macOS alone:

Device Mapping At 50 %
macOS built-in speakers dB = −63.5 · (1 − √s) −18.6 dB
A third-party macOS virtual card dB = −64 + 64·s (dB-linear) −32.0 dB
AudioHub's own macOS virtual device dB = −64 + 64·s² −48.0 dB
Windows software endpoint amplitude = s^1.745 −10.5 dB

Square root, linear, squared, and a power law. The curvature does not even point the same way. Windows is internally inconsistent too — endpoints with hardware volume follow their own curve again.

One structural difference is worth noting: the Windows mapping is independent of the device's dB range, while the macOS mapping scales with it. These are two different design philosophies, not two parameterisations of one idea.


The impossibility theorem

Let T_ours be the taper of the virtual device we control, and T_peer the taper of the peer's real device, which the hardware vendor and the OS control.

  • Equal loudness requires T_ours(s₁) = T_peer(s₂).
  • Equal slider numbers requires s₁ = s₂.

Both hold simultaneously if and only if T_ours ≡ T_peer.

Measurement has already shown T_peer is four different functions across four devices. No single T_ours can equal all of them.

So this is not an engineering problem where a better implementation gets you both. It is a product decision where one must be chosen.

The choice: loudness wins

You hear loudness. Slider numbers only matter if two people sit at two machines and read numbers to each other. An 8–11 dB error is roughly a factor of two in perceived volume; a mismatched percentage merely looks odd.

The visible consequence

Because loudness is preserved rather than position, the two sliders will show different numbers. Sending amplitude 0.5 to a Windows endpoint makes its slider read about 67 %, not 50 %. This is the system working correctly.


How it is implemented

The wire carries amplitude, not slider position and not decibels.

  • Amplitude represents mute exactly. Mute is 0.0. In the decibel domain mute is −∞, which forces a floor convention, and every device has a different floor (−63.5, −64, −96 were all observed) — meaning the number on the wire would not equal the number in effect.
  • It is bit-for-bit the number the software-gain fallback already uses. When a peer's device cannot be adjusted at all, AudioHub applies gain in software on the sending side, using the wire value directly as a multiplier. Sharing one representation makes the two paths structurally incapable of differing in loudness.
  • Precision is a non-issue. 32-bit float relative precision works out to around 5 × 10⁻⁷ dB.

The one trap: never compare amplitudes to decide "did the volume change much". Equal steps in amplitude are wildly unequal steps in perception.


Volume in each mode

Mode B (virtual devices). Adjusting the virtual device's volume in the system adjusts the peer's real device. This is the default semantic and needs no setting.

Mode A (driverless). Volume sync is optional and off by default. When enabled, this machine's system output volume and the peer's real output follow each other, with the peer taking precedence: on connect this machine adopts the peer's value, and if both change at once the peer's value wins.

Mute follows volume — with one exception. If "mute this machine's output" is also enabled, only volume syncs and mute does not. Otherwise the one-shot mute performed at connection time would travel down the sync link and silence the peer as well.

That mute is a one-time action, not a persistent state: it fires once when the speaker path is established. If you unmute afterwards, that is taken to mean you want this machine audible too, and it is not re-applied until the next time a path is established. On capture backends where the tap sits after volume (Windows device loopback), it is skipped entirely — muting there would also silence the mirror being sent to the peer.


Audio is sent at full scale

The stream itself carries unmodified, full-scale audio. Volume is applied at the endpoints, not in transit.

One exception: when the peer's real device does not support volume adjustment at all — a macOS aggregate or multi-output device, typically — the consumer's virtual device manages volume itself, and the gain is applied in software on the sending side. In that case, and only that case, the audio on the wire already has volume applied.

The UI states which of the two is in force rather than leaving a slider that silently does nothing.


A note on measurement method

If you go looking at this yourself: on macOS, the Core Audio properties that translate between scalar and decibels do not agree with the device's own behaviour. On built-in speakers the translation property reports a linear curve that contradicts the device's actual state by more than 15 dB, and on virtual devices it simply returns an error.

Trust only set-then-read-back. Every figure on this page was obtained that way.

Clone this wiki locally