Process the microphone like TS3, and tell it about key presses

The capture path now runs the ported WebRTC chain in place of the
home-made noise, typing and gain stages, which are removed.

- As in TS3, the speech detector judges the raw microphone signal, while
  the level meter and volume gate see the processed one.
- Noise removal takes TS3's four levels (6, 12, 18 or 21 dB); a stored
  0..1 level falls back to TS3's default of 12 dB.
- Every key press from the global input hook reaches the connected
  microphones and the microphone test, so typing attenuation engages
  while the user types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 20:47:04 +00:00
parent de5f7da236
commit 1d61f458ae
12 changed files with 102 additions and 588 deletions

View File

@@ -84,9 +84,13 @@ final class DevicesPanel extends FormPanel {
denoiseCheck = new JCheckBox("Remove background noise", settings.denoise);
denoiseCheck.setToolTipText("Attempt to filter out background noises.");
denoiseLevel = new JSlider(0, 100, (int) Math.round(settings.denoiserLevel * 100));
// TS3's four steps, 6/12/18/21 dB of suppression.
denoiseLevel = new JSlider(0, 3, Math.max(0, Math.min(3, settings.denoiserLevel)));
denoiseLevel.setMajorTickSpacing(1);
denoiseLevel.setPaintTicks(true);
denoiseLevel.setSnapToTicks(true);
limitWidth(denoiseLevel, SLIDER_WIDTH);
denoiseLevel.setToolTipText("Higher = more aggressive noise removal.");
denoiseLevel.setToolTipText("How much steady noise to remove: 6, 12, 18 or 21 dB.");
typingCheck = new JCheckBox("Typing attenuation", settings.typingAttenuation);
typingCheck.setToolTipText("<html><b>Typing attenuation</b> tries to detect and "
+ "reduce the sounds made by typing.</html>");
@@ -113,7 +117,7 @@ final class DevicesPanel extends FormPanel {
denoiseLevel.setEnabled(denoiseCheck.isSelected());
applyLive.accept(m -> {
m.setNoiseSuppression(denoiseCheck.isSelected());
m.setDenoiserLevel(denoiseLevel.getValue() / 100.0);
m.setDenoiserLevel(denoiseLevel.getValue());
m.setTypingAttenuation(typingCheck.isSelected());
m.setAgc(agcCheck.isSelected());
});
@@ -122,7 +126,7 @@ final class DevicesPanel extends FormPanel {
typingCheck.addActionListener(e -> syncNoise.run());
agcCheck.addActionListener(e -> syncNoise.run());
denoiseLevel.addChangeListener(e ->
applyLive.accept(m -> m.setDenoiserLevel(denoiseLevel.getValue() / 100.0)));
applyLive.accept(m -> m.setDenoiserLevel(denoiseLevel.getValue())));
syncNoise.run();
mutedWarningCheck = new JCheckBox("Beep when talking while muted", settings.mutedTalkWarning);
@@ -148,7 +152,7 @@ final class DevicesPanel extends FormPanel {
target.inputVolume = inputGain.getValue() / 100.0;
target.outputVolume = outputVol.getValue() / 100.0;
target.denoise = denoiseCheck.isSelected();
target.denoiserLevel = denoiseLevel.getValue() / 100.0;
target.denoiserLevel = denoiseLevel.getValue();
target.typingAttenuation = typingCheck.isSelected();
target.agc = agcCheck.isSelected();
target.mutedTalkWarning = mutedWarningCheck.isSelected();

View File

@@ -10,6 +10,7 @@ import com.ts3client.hotkey.Hotkeys;
import com.ts3client.hotkey.desktop.DesktopInputHooks;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Owns the hotkey machinery for the UI: the stored bindings, the matching engine and
@@ -22,11 +23,29 @@ final class HotkeyService {
private final HotkeyEngine engine;
private final GlobalInputHook hook;
private final HotkeyArguments arguments;
private final List<Runnable> keyPressListeners = new CopyOnWriteArrayList<>();
HotkeyService(HotkeyEngine.Handler handler, HotkeyArguments arguments) {
this.arguments = arguments;
engine = new HotkeyEngine(hotkeys, handler);
hook = DesktopInputHooks.start(engine);
hook = DesktopInputHooks.start((key, pressed) -> {
engine.onInput(key, pressed);
if (pressed && key.device() == HotkeyKey.Device.KEYBOARD) {
for (Runnable l : keyPressListeners) l.run();
}
});
}
/**
* Runs {@code l} on the input hook's thread for every key going down anywhere on the
* system, which is what typing attenuation listens for. Keep it cheap.
*/
void addKeyPressListener(Runnable l) {
keyPressListeners.add(l);
}
void removeKeyPressListener(Runnable l) {
keyPressListeners.remove(l);
}
/** The values this action's parameter can take right now, for the action tree. */

View File

@@ -95,6 +95,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
this.soundPlayer = audio.createSoundPlayer(settings);
this.sounds.setPlayer(soundPlayer);
this.hotkeys = new HotkeyService(new HotkeyActions(this), this::hotkeyArgumentChoices);
hotkeys.addKeyPressListener(() -> SwingUtilities.invokeLater(this::keyPressed));
contacts.addListener(() -> SwingUtilities.invokeLater(this::contactsChanged));
setIconImages(Icons.appImages());
@@ -482,6 +483,15 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
tab.connection().getMicrophone().setPushToTalk(talking);
}
/** Tells every connected microphone about a key press, as TS3 tells all its connections. */
private void keyPressed() {
for (ServerTab tab : tabs) {
if (tab.isConnected() && tab.connection().getMicrophone() != null) {
tab.connection().getMicrophone().keyPressed();
}
}
}
void moveMicrophoneToSelectedTab() {
if (selected != null && selected.isConnected()) setMicTab(selected);
updateToolbar();

View File

@@ -10,6 +10,7 @@ import com.ts3client.sound.SoundNotifier;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.JTabbedPane;
import java.awt.BorderLayout;
import java.awt.Dimension;
@@ -83,11 +84,17 @@ public final class SettingsDialog extends JDialog {
getContentPane().add(tabs, BorderLayout.CENTER);
getContentPane().add(buttons, BorderLayout.SOUTH);
// The microphone test hears typing too, so typing attenuation can be tried out here.
Runnable testKeyPress = () -> SwingUtilities.invokeLater(
() -> voiceActivationPanel.configureTest(VoiceInput::keyPressed));
hotkeys.addKeyPressListener(testKeyPress);
Dialogs.closeOnEscape(this, this::cancel);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosed(java.awt.event.WindowEvent e) {
hotkeys.removeKeyPressListener(testKeyPress);
voiceActivationPanel.stopTest();
}
});