Report preparation
What happened?
Operating SSB on a Hermes-Lite 2, the transmitted signal rose on its own during
pauses. In the operator's words:
the ssb tx signal went way up without me saying anything. When not talking you
expect ssb to be close to silent.
Reproduced on the operator's own voice, AirPods in both directions, keyed
against a simulator, mic slider 70:
- The ALC's gain climbs 16.65 → 30.70 dB inside a single pause.
- On two of three pauses the output contrast between pause and speech collapses
to 0.00 dB, with the transmitted level pinned at −1.41 dBFS — which is
alcTargetPeak 0.85 exactly.
What triggered it was not the room. Their AirPods' steady floor is clean:
p50 −60.33 dBFS even at slider 90, fifteen dB below the −45 dBFS hold
threshold, with zero blocks above it. What crossed were transients inside
the pauses — block maxima of −31.6 and −23.9 dBFS against a steady floor
near −65.
At −31.6 dBFS the makeup the ALC wants is alcTargetPeak / blockPeak, about
+30 dB. So a breath, or a shift in the chair, is treated as a syllable and
lifted toward full modulation — and because only a louder block can bring
the gain back down, it stays there for the rest of the pause.
That is the operator's complaint, precisely: not that a noisy room gets
amplified, but that one small sound during a pause pins the transmitted noise
at speech level until they speak again.
The hold is not broken — it is one-way. At slider 90, where nothing crossed
the threshold at all, it worked exactly as designed and the gain froze to the
second decimal (27.27 → 27.27, 27.46 → 27.46) with contrast held at
28.5–28.7 dB. The failure is not that the hold fails to engage. It is that a
single crossing is irreversible.
The same mechanism, the other way in
On a hotter microphone the steady floor itself crosses. Reproduced in a digital
loopback with a −53 dBFS room floor and the slider at 100, which raises that
floor after the voice strip:
| measured at the ALC |
pause |
speech |
separation |
| input |
−37.05 dBFS |
−12.4 dBFS |
20.5 dB |
| output |
−1.38 dBFS |
−1.05 dBFS |
0.33 dB |
An input separation of 20.5 dB leaves as 0.33 dB.
Two devices, two routes, one mechanism:
An absolute threshold on an instantaneous block peak cannot distinguish a
breath from a syllable.
Mechanism
In Hl2TxDsp, the makeup half of the ALC lifts each block toward
alcTargetPeak (0.85, −1.4 dBFS) with a ceiling of alcMaxGainDb (40 dB)
and a 500 ms release. It stops lifting only while
const bool held = !reducing && !clientLeveled && blockPeak < holdThreshold;
where holdThreshold comes from alcHoldBelowDbfs = −45.0.
Three properties combine:
- The threshold is absolute, not relative to the programme. The same file
notes that "speech sits around -32 dBFS", so anything between −45 and
−32 dBFS is treated as loud enough to be speech and is lifted toward 0.85 —
a ~13 dB window in which a noise floor is indistinguishable from a talker.
- It is applied to an instantaneous block peak. A breath at −31.6 dBFS and
a syllable at −31.6 dBFS are the same number, so the stage cannot tell them
apart — and it lifts both toward full modulation.
- Within a pause there is no path down. The gain ratchets up on any block
that crosses, and the hold only prevents further lifting; it does not undo
what has been applied. Only a louder block can pull the gain back down,
and a pause is precisely what does not contain one. Reduction itself is never
held, so the gain does come back at the next burst — the fault is not a level
that runs away across the over, it is noise transmitted at speech level for
the whole of every pause.
A second route reaches the same place on hotter microphones: the threshold is
compared after the operator's mic gain, and the slider exists partly to
carry a quiet microphone over this very threshold — the code says so — so
raising gain to be heard is also what pushes a steady room floor across it.
That is the route the loopback above reproduces. Both routes are the same
defect: an absolute threshold on an instantaneous peak, with no way back
down.
What did you expect?
Pauses to be close to silent on SSB, and any automatic level control ahead of a
transmitter to reduce gain rather than add it.
On the name, structurally rather than rhetorically
In amateur-radio usage an ALC — internal or external — is a feedback loop
around a power amplifier that gain-limits to protect the finals. This stage
has no amplifier in its loop: it acts on audio before the modulator. It applies
up to 40 dB of upward makeup, and it is continuously engaged rather than
rarely.
Whatever this stage is, it is not an ALC, and the name is plausibly why it has
no operator controls — a protective limiter would not need any. It is a
leveller, and calling it one would make the design question visible.
The application cannot know whether the capture device levels
Whether a capture device applies its own AGC is not knowable to the
application, and differs by device — which the two devices measured here
already demonstrate. The operator transmits on AirPods, whose floor sits at
p50 −60.33 dBFS, fifteen dB below the hold threshold, and which crosses on
transients rather than on its floor. A USB webcam microphone on the same machine
(Logitech 4K Stream Edition) behaves differently again; 30 s of silence from it,
in 3-second blocks:
RMS -67.63 -67.62 -68.03 -67.81 -67.74 -67.73 -67.79 -68.07 dBFS
peak -53.16 -54.75 -55.35 -54.75 -54.19 -54.75 -54.75 -55.35 dBFS
Flat to 0.45 dB RMS and 2.2 dB peak over thirty seconds — no gain riding at
all. A winding AGC climbs during sustained silence, which is exactly the
signature the stage under discussion shows. This device does not do it.
What that measurement does not show: it does not prove the absence of AGC.
An AGC may legitimately hold during pure silence — the stage complained about
here does exactly that below its own threshold. Establishing it either way needs
a level step: present the same signal at two very different acoustic levels
and check whether the recorded ratio matches the presented one. That test has
not been done, so for this device the question is open.
So a fixed 40 dB of makeup, applied blind, is wrong in both directions:
- On a device that levels, it is a second loop chasing the first.
- On a device that does not — like the one measured above — the makeup is doing
real work, and the fault is not that it exists. The fault is that it cannot
be told to stop during pauses, and cannot be seen or adjusted at all.
None of this means nothing is wanted ahead of an SSB transmitter. SSB is
peak-power limited while intelligibility rides on average power, and unprocessed
voice runs ~17 dB peak-to-average, so compression, a genuine protective limiter
and band-limiting are all still wanted. Levelling and compression are different
jobs: levelling makes the average consistent, compression changes the crest
factor.
And AetherSDR already has all three, properly. AetherVoice provides a
compressor, gate, EQ, de-esser and final limiter — visible, adjustable,
operator-ordered. (They ship off: ClientFinalLimiterTxEnabled defaults to
"False" like the rest of the strip, so nothing in this issue assumes any of
them is running.) Hl2TxDsp adds a fourth stage that is invisible and
unadjustable, whose behaviour depends on a property of the capture device that
the application never establishes.
Steps to reproduce
- Feed the HL2 TX path a signal with a quiet noise floor and speech-like
bursts — a floor around −53 dBFS with bursts peaking near −12 dBFS
reproduces it.
- Set the microphone gain slider high enough that the floor arrives at
Hl2TxDsp above −45 dBFS (slider 100 with the above).
- Compare pause-to-speech separation at the input of the ALC and at its output.
Here that is 20.5 dB in and 0.33 dB out.
Operator workaround, which does work: enabling the client gate/expander ahead
of the ALC drops the floor at the ALC by 12.6 dB (−37.05 → −49.69 dBFS), back
under the hold threshold, and is transparent on speech at ordinary settings
(level +0.09 dB, spread 19.71 → 19.53 dB). It is a workaround rather than a fix —
it requires knowing the interaction exists.
AetherSDR version
26.9.1
Radio model & firmware
Hermes-Lite 2, gateware 20231230_74p2_883a338
Operating system
macOS
OS version and hardware
macOS 26.6.2, Apple M2, Qt 6.8.3
Suggested fix — converge on the reference implementation
The library AetherSDR already vendors makes exactly the distinction this stage
collapses. In third_party/wdsp/upstream/TXA.c, create_txa() builds
two wcpagc instances at two points via create_wcpagc, with these constructor arguments and
these comments:
txa[channel].leveler.p = create_wcpagc (
0, // run - OFF by default
...
0.500, // tau_decay
1.778, // max_gain (+5.0 dB)
txa[channel].alc.p = create_wcpagc (
1, // run - always ON
...
0.010, // tau_decay
1.0, // max_gain (+0.00 dB)
The stage named ALC is always on and has a unity ceiling — it is
structurally incapable of adding gain. The stage that can add gain is a
leveler, off by default, capped at +5 dB, with a 500 ms decay.
piHPSDR wires the same split, and goes further:
SetTXAALCSt(tx->id, 1); // turn it on (always on)
SetTXALevelerTop(tx->id, 5.0);
SetTXALevelerSt(tx->id, tx_leveler);
tx_leveler has three mentions in the whole tree — int tx_leveler=0;, an
extern declaration, and the read above. Nothing ever assigns it. So as
piHPSDR configures WDSP, the leveller is off and unreachable and the ALC cannot
amplify: there is no upward makeup anywhere in that transmit chain, and it
is a widely used client for this radio.
Where its operators get level instead is the microphone gain, which piHPSDR
ranges −12 dB to +50 dB. That pairing is the point: no automatic makeup, and
a wide manual control.
Hl2TxDsp has one stage that is always on, named ALC, and ceilinged at +40 dB
— the reference's two roles merged, keeping the always-on property of the
protective one and the gain range of the optional one.
The chain today, and proposed
today: mic → AetherVoice strip (48 kHz, OFF by default) → 24 kHz
→ m_micGain (slider, NOT persisted)
→ "ALC": always on, up to +40 dB makeup, 500 ms release,
hold below −45 dBFS absolute
→ hard clamp → phasing modulator → EP2
proposed: mic → AetherVoice strip (UNCHANGED) → 24 kHz
→ m_micGain (slider, PERSISTED)
→ LEVELLER: OFF by default, ceiling ~5 dB, operator-visible
→ ALC: always on, ceiling UNITY — protection only, cannot amplify
→ hard clamp → phasing modulator → EP2
Four changes, in dependency order
Each is a regression without the ones before it, which is why the order matters.
1. Persist the mic slider. The control already exists — registered as
phone.micLevel (a 0–100 slider under Phone/CW), MIDI-bindable as micgain,
and already reported in the health snapshot. It is simply not restored:
the mic-level setter in Hl2Backend.cpp says so itself, explaining why 50
must map to unity —
"TransmitModel constructs m_micLevel at 50 and nothing restores it at
startup". Any operating procedure that begins "set mic gain once" is defeated
by a control that resets to unity every launch, and every change below assumes
the operator's gain survives.
Note what that comment implies: the unrestored slider is not an oversight. The
code has already reasoned about the consequence and designed around it, pinning
50 to unity because nothing restores it. So this change asks you to remove a
constraint you documented, not to notice a bug you missed — and the mapping is
free to change once the value persists.
2. Give alcGain a meter. It currently drives none —
"alcGain drives no meter — TX:ALC is fed from alcPeak above" — and the gauge
labelled TX:ALC shows a post-ALC level, which Hl2TxDsp::processAudioBlock describes in
its own words as a meter that "sits pinned near the target by definition and
tells the operator nothing — it reports the ALC's success, not their input
level." Without this, changes 3 and 4 are invisible to the operator: they
would alter behaviour with no instrument showing what changed.
There is prior art here too. WDSP's transmit chain carries seven meters —
micmeter, eqmeter, lvlrmeter, cfcmeter, compmeter, alcmeter, outmeter
(the meter struct in TXA.h) — and its TXA_* readable enum exposes
TXA_LVLR_GAIN and TXA_ALC_GAIN as separate quantities. Keep the existing level gauge but name it for what it
shows, and let TX:ALC carry the gain quantity every other rig means by that
label.
3. alcMaxGainDb 40.0 → 0.0, together with a wider mic-gain range. The ALC
becomes what its name means, and what the reference's ALC is: protection only,
structurally unable to amplify.
These two must land together, and here is the arithmetic. The file puts
speech at −32 dBFS and alcTargetPeak at 0.85, which is −1.4 dBFS — a gap of
30.6 dB. The mic slider spans ±20 dB ("0.4 dB per slider step"). Remove the
makeup without widening that range and the chain is 10.6 dB short at maximum
slider: the symptom becomes "the radio went quiet and there is no control
left", which is the same fault as the original pointing the other way. piHPSDR,
which as shown above has no makeup at all, gives its operators −12 to +50 dB.
And the change is smaller than it looks. The whole content of the
clientLeveled special case is
const double ceiling = clientLeveled ? 1.0
: std::pow(10.0, m_config.alcMaxGainDb / 20.0);
With alcMaxGainDb = 0 the ceiling is 1.0 on both branches. The branch, the
!clientLeveled term in the hold, and the entire one-sided TCI/DAX contract
stop doing anything and can be deleted. The fix removes more code than it
adds.
4. Add an optional leveller — off by default, ceiling ~5 dB, operator-visible
— to carry the makeup that change 3 removes, for operators whose microphone
genuinely needs it.
Why change 4 does not belong in AetherVoice
The strip is off by default. A leveller reachable only once an operator has
found and enabled an advanced panel is not a leveller, it is a feature. It has
to live in the always-on path, beside the ALC, which is where the reference puts
it.
One honest deviation: WDSP's order is expander → leveller → compressor, and
since AetherVoice holds both the expander and the compressor upstream, a
leveller placed in Hl2TxDsp lands after the compressor rather than before
it. That is a real difference from the reference and is named here rather than
left for a reviewer to find. It is a consequence of where the two chains meet,
not of the proposal.
On WDSP's TX chain — not what is being asked for
Hl2TxDsp's own header explains why this backend does not use TXA: driven from
this configuration a TXA channel "returned Underrun on most blocks and zeros on
the rest", two speculative fixes at the initialisation sequence made things
worse, and the deciding factor was that "the failure mode is SILENT" where a
fifty-line phasing modulator's correctness is measurable.
That reasoning is sound and this issue does not ask for it to be revisited.
The suggestion above changes one ceiling in the existing chain.
What this issue does not claim
- One operator, one voice, one capture device. The live figures are three
passes from a single operator on AirPods. The loopback figures are authored to
match a different, hotter arrangement. The two devices measured here behave
differently — one crosses the threshold on transients, the other on its steady
floor — and that difference is the point rather than a weakness, but neither is
a survey.
- Measurement windows differ and are stated. Floor, output and contrast are
tap-derived over the first 14.9 s of each pass; the gain trajectory covers the
full 30 s. The pauses quoted above fall inside the tap window.
- Whether either capture device applies AGC is not established. The 30 s
time-resolved silence measurement above is of the webcam microphone. The
AirPods the operator transmits on were measured at rest too — 6 s at their
native 24 kHz, peak −57.05 dBFS, RMS −80.10 dBFS — but as a single figure pair
and not as a block series, so for that device flatness over time is not shown
either way. And even for the device where it is, flatness rules out a
winding AGC but not a holding one — an AGC may sit still on silence, exactly
as the stage described here does below its own threshold. The level-step test
that would settle it has not been run for either device. The argument above is
deliberately built so that it does not depend on the answer.
- No claim that computer microphones in general level their audio. General
sources say device-side AGC is common and often hard to disable; that is
background, not evidence about any particular device, and the one device
measured here does not appear to wind.
- No claim that other clients avoid this. Only that WDSP's leveller uses a
relative, operator-visible threshold where this one is absolute and hidden.
Whether other implementations enable their leveller by default is not
established here.
- Scope is the HL2 backend. Flex and Icom modulate in the radio and never run
this code. ANAN declares host modulation and would inherit the same decision
when its transmit path lands.
Report preparation
What happened?
Operating SSB on a Hermes-Lite 2, the transmitted signal rose on its own during
pauses. In the operator's words:
Reproduced on the operator's own voice, AirPods in both directions, keyed
against a simulator, mic slider 70:
to 0.00 dB, with the transmitted level pinned at −1.41 dBFS — which is
alcTargetPeak0.85 exactly.What triggered it was not the room. Their AirPods' steady floor is clean:
p50 −60.33 dBFS even at slider 90, fifteen dB below the −45 dBFS hold
threshold, with zero blocks above it. What crossed were transients inside
the pauses — block maxima of −31.6 and −23.9 dBFS against a steady floor
near −65.
At −31.6 dBFS the makeup the ALC wants is
alcTargetPeak / blockPeak, about+30 dB. So a breath, or a shift in the chair, is treated as a syllable and
lifted toward full modulation — and because only a louder block can bring
the gain back down, it stays there for the rest of the pause.
That is the operator's complaint, precisely: not that a noisy room gets
amplified, but that one small sound during a pause pins the transmitted noise
at speech level until they speak again.
The hold is not broken — it is one-way. At slider 90, where nothing crossed
the threshold at all, it worked exactly as designed and the gain froze to the
second decimal (27.27 → 27.27, 27.46 → 27.46) with contrast held at
28.5–28.7 dB. The failure is not that the hold fails to engage. It is that a
single crossing is irreversible.
The same mechanism, the other way in
On a hotter microphone the steady floor itself crosses. Reproduced in a digital
loopback with a −53 dBFS room floor and the slider at 100, which raises that
floor after the voice strip:
An input separation of 20.5 dB leaves as 0.33 dB.
Two devices, two routes, one mechanism:
Mechanism
In
Hl2TxDsp, the makeup half of the ALC lifts each block towardalcTargetPeak(0.85, −1.4 dBFS) with a ceiling ofalcMaxGainDb(40 dB)and a 500 ms release. It stops lifting only while
where
holdThresholdcomes fromalcHoldBelowDbfs = −45.0.Three properties combine:
notes that "speech sits around -32 dBFS", so anything between −45 and
−32 dBFS is treated as loud enough to be speech and is lifted toward 0.85 —
a ~13 dB window in which a noise floor is indistinguishable from a talker.
a syllable at −31.6 dBFS are the same number, so the stage cannot tell them
apart — and it lifts both toward full modulation.
that crosses, and the hold only prevents further lifting; it does not undo
what has been applied. Only a louder block can pull the gain back down,
and a pause is precisely what does not contain one. Reduction itself is never
held, so the gain does come back at the next burst — the fault is not a level
that runs away across the over, it is noise transmitted at speech level for
the whole of every pause.
A second route reaches the same place on hotter microphones: the threshold is
compared after the operator's mic gain, and the slider exists partly to
carry a quiet microphone over this very threshold — the code says so — so
raising gain to be heard is also what pushes a steady room floor across it.
That is the route the loopback above reproduces. Both routes are the same
defect: an absolute threshold on an instantaneous peak, with no way back
down.
What did you expect?
Pauses to be close to silent on SSB, and any automatic level control ahead of a
transmitter to reduce gain rather than add it.
On the name, structurally rather than rhetorically
In amateur-radio usage an ALC — internal or external — is a feedback loop
around a power amplifier that gain-limits to protect the finals. This stage
has no amplifier in its loop: it acts on audio before the modulator. It applies
up to 40 dB of upward makeup, and it is continuously engaged rather than
rarely.
Whatever this stage is, it is not an ALC, and the name is plausibly why it has
no operator controls — a protective limiter would not need any. It is a
leveller, and calling it one would make the design question visible.
The application cannot know whether the capture device levels
Whether a capture device applies its own AGC is not knowable to the
application, and differs by device — which the two devices measured here
already demonstrate. The operator transmits on AirPods, whose floor sits at
p50 −60.33 dBFS, fifteen dB below the hold threshold, and which crosses on
transients rather than on its floor. A USB webcam microphone on the same machine
(Logitech 4K Stream Edition) behaves differently again; 30 s of silence from it,
in 3-second blocks:
Flat to 0.45 dB RMS and 2.2 dB peak over thirty seconds — no gain riding at
all. A winding AGC climbs during sustained silence, which is exactly the
signature the stage under discussion shows. This device does not do it.
What that measurement does not show: it does not prove the absence of AGC.
An AGC may legitimately hold during pure silence — the stage complained about
here does exactly that below its own threshold. Establishing it either way needs
a level step: present the same signal at two very different acoustic levels
and check whether the recorded ratio matches the presented one. That test has
not been done, so for this device the question is open.
So a fixed 40 dB of makeup, applied blind, is wrong in both directions:
real work, and the fault is not that it exists. The fault is that it cannot
be told to stop during pauses, and cannot be seen or adjusted at all.
None of this means nothing is wanted ahead of an SSB transmitter. SSB is
peak-power limited while intelligibility rides on average power, and unprocessed
voice runs ~17 dB peak-to-average, so compression, a genuine protective limiter
and band-limiting are all still wanted. Levelling and compression are different
jobs: levelling makes the average consistent, compression changes the crest
factor.
And AetherSDR already has all three, properly. AetherVoice provides a
compressor, gate, EQ, de-esser and final limiter — visible, adjustable,
operator-ordered. (They ship off:
ClientFinalLimiterTxEnableddefaults to"False"like the rest of the strip, so nothing in this issue assumes any ofthem is running.)
Hl2TxDspadds a fourth stage that is invisible andunadjustable, whose behaviour depends on a property of the capture device that
the application never establishes.
Steps to reproduce
bursts — a floor around −53 dBFS with bursts peaking near −12 dBFS
reproduces it.
Hl2TxDspabove −45 dBFS (slider 100 with the above).Here that is 20.5 dB in and 0.33 dB out.
Operator workaround, which does work: enabling the client gate/expander ahead
of the ALC drops the floor at the ALC by 12.6 dB (−37.05 → −49.69 dBFS), back
under the hold threshold, and is transparent on speech at ordinary settings
(level +0.09 dB, spread 19.71 → 19.53 dB). It is a workaround rather than a fix —
it requires knowing the interaction exists.
AetherSDR version
26.9.1
Radio model & firmware
Hermes-Lite 2, gateware
20231230_74p2_883a338Operating system
macOS
OS version and hardware
macOS 26.6.2, Apple M2, Qt 6.8.3
Suggested fix — converge on the reference implementation
The library AetherSDR already vendors makes exactly the distinction this stage
collapses. In
third_party/wdsp/upstream/TXA.c,create_txa()buildstwo
wcpagcinstances at two points viacreate_wcpagc, with these constructor arguments andthese comments:
The stage named ALC is always on and has a unity ceiling — it is
structurally incapable of adding gain. The stage that can add gain is a
leveler, off by default, capped at +5 dB, with a 500 ms decay.
piHPSDR wires the same split, and goes further:
tx_levelerhas three mentions in the whole tree —int tx_leveler=0;, anexterndeclaration, and the read above. Nothing ever assigns it. So aspiHPSDR configures WDSP, the leveller is off and unreachable and the ALC cannot
amplify: there is no upward makeup anywhere in that transmit chain, and it
is a widely used client for this radio.
Where its operators get level instead is the microphone gain, which piHPSDR
ranges −12 dB to +50 dB. That pairing is the point: no automatic makeup, and
a wide manual control.
Hl2TxDsphas one stage that is always on, named ALC, and ceilinged at +40 dB— the reference's two roles merged, keeping the always-on property of the
protective one and the gain range of the optional one.
The chain today, and proposed
Four changes, in dependency order
Each is a regression without the ones before it, which is why the order matters.
1. Persist the mic slider. The control already exists — registered as
phone.micLevel(a 0–100 slider under Phone/CW), MIDI-bindable asmicgain,and already reported in the health snapshot. It is simply not restored:
the mic-level setter in
Hl2Backend.cppsays so itself, explaining why 50must map to unity —
"TransmitModel constructs m_micLevel at 50 and nothing restores it at
startup". Any operating procedure that begins "set mic gain once" is defeated
by a control that resets to unity every launch, and every change below assumes
the operator's gain survives.
Note what that comment implies: the unrestored slider is not an oversight. The
code has already reasoned about the consequence and designed around it, pinning
50 to unity because nothing restores it. So this change asks you to remove a
constraint you documented, not to notice a bug you missed — and the mapping is
free to change once the value persists.
2. Give
alcGaina meter. It currently drives none —"alcGain drives no meter — TX:ALC is fed from alcPeak above" — and the gauge
labelled TX:ALC shows a post-ALC level, which
Hl2TxDsp::processAudioBlockdescribes inits own words as a meter that "sits pinned near the target by definition and
tells the operator nothing — it reports the ALC's success, not their input
level." Without this, changes 3 and 4 are invisible to the operator: they
would alter behaviour with no instrument showing what changed.
There is prior art here too. WDSP's transmit chain carries seven meters —
micmeter, eqmeter, lvlrmeter, cfcmeter, compmeter, alcmeter, outmeter(the meter struct in
TXA.h) — and itsTXA_*readable enum exposesTXA_LVLR_GAINandTXA_ALC_GAINas separate quantities. Keep the existing level gauge but name it for what itshows, and let TX:ALC carry the gain quantity every other rig means by that
label.
3.
alcMaxGainDb40.0 → 0.0, together with a wider mic-gain range. The ALCbecomes what its name means, and what the reference's ALC is: protection only,
structurally unable to amplify.
These two must land together, and here is the arithmetic. The file puts
speech at −32 dBFS and
alcTargetPeakat 0.85, which is −1.4 dBFS — a gap of30.6 dB. The mic slider spans ±20 dB ("0.4 dB per slider step"). Remove the
makeup without widening that range and the chain is 10.6 dB short at maximum
slider: the symptom becomes "the radio went quiet and there is no control
left", which is the same fault as the original pointing the other way. piHPSDR,
which as shown above has no makeup at all, gives its operators −12 to +50 dB.
And the change is smaller than it looks. The whole content of the
clientLeveledspecial case isWith
alcMaxGainDb = 0the ceiling is 1.0 on both branches. The branch, the!clientLeveledterm in the hold, and the entire one-sided TCI/DAX contractstop doing anything and can be deleted. The fix removes more code than it
adds.
4. Add an optional leveller — off by default, ceiling ~5 dB, operator-visible
— to carry the makeup that change 3 removes, for operators whose microphone
genuinely needs it.
Why change 4 does not belong in AetherVoice
The strip is off by default. A leveller reachable only once an operator has
found and enabled an advanced panel is not a leveller, it is a feature. It has
to live in the always-on path, beside the ALC, which is where the reference puts
it.
One honest deviation: WDSP's order is expander → leveller → compressor, and
since AetherVoice holds both the expander and the compressor upstream, a
leveller placed in
Hl2TxDsplands after the compressor rather than beforeit. That is a real difference from the reference and is named here rather than
left for a reviewer to find. It is a consequence of where the two chains meet,
not of the proposal.
On WDSP's TX chain — not what is being asked for
Hl2TxDsp's own header explains why this backend does not use TXA: driven fromthis configuration a TXA channel "returned Underrun on most blocks and zeros on
the rest", two speculative fixes at the initialisation sequence made things
worse, and the deciding factor was that "the failure mode is SILENT" where a
fifty-line phasing modulator's correctness is measurable.
That reasoning is sound and this issue does not ask for it to be revisited.
The suggestion above changes one ceiling in the existing chain.
What this issue does not claim
passes from a single operator on AirPods. The loopback figures are authored to
match a different, hotter arrangement. The two devices measured here behave
differently — one crosses the threshold on transients, the other on its steady
floor — and that difference is the point rather than a weakness, but neither is
a survey.
tap-derived over the first 14.9 s of each pass; the gain trajectory covers the
full 30 s. The pauses quoted above fall inside the tap window.
time-resolved silence measurement above is of the webcam microphone. The
AirPods the operator transmits on were measured at rest too — 6 s at their
native 24 kHz, peak −57.05 dBFS, RMS −80.10 dBFS — but as a single figure pair
and not as a block series, so for that device flatness over time is not shown
either way. And even for the device where it is, flatness rules out a
winding AGC but not a holding one — an AGC may sit still on silence, exactly
as the stage described here does below its own threshold. The level-step test
that would settle it has not been run for either device. The argument above is
deliberately built so that it does not depend on the answer.
sources say device-side AGC is common and often hard to disable; that is
background, not evidence about any particular device, and the one device
measured here does not appear to wind.
relative, operator-visible threshold where this one is absolute and hidden.
Whether other implementations enable their leveller by default is not
established here.
this code. ANAN declares host modulation and would inherit the same decision
when its transmit path lands.