Files
ts3java/ts3-client/swing/src/main/java/com/ts3client/ui/DevicesPanel.java
ericek111 d722377508 Split SettingsDialog into focused tab panels
Extract the Playback/Capture and Voice Activation tabs (device pickers,
gain/volume sliders, noise reduction, VAD/PTT tuning, microphone test)
into DevicesPanel and VoiceActivationPanel, alongside the existing
NotificationsPanel/HotkeysPanel/IconPackPanel/ClientVersionPanel. A new
FormPanel base holds the shared GridBagLayout scaffolding. SettingsDialog
is now a thin coordinator (753 -> 187 lines); its public API and all
settings read/write behavior are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 13:37:51 +00:00

161 lines
6.8 KiB
Java

package com.ts3client.ui;
import com.ts3client.audio.VoiceInput;
import com.ts3client.audio.VoiceOutput;
import com.ts3client.audio.desktop.AudioDevices;
import com.ts3client.config.Settings;
import javax.swing.Box;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JSlider;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import java.util.List;
import java.util.function.Consumer;
/**
* Options dialog's "Playback / Capture" tab: device pickers, gain/volume, and the
* noise-reduction pre-processing options. Gain and pre-processing changes are pushed
* live via {@code applyLive}, which reaches both the connected microphone and the
* dialog's own microphone test.
*/
final class DevicesPanel extends FormPanel {
private final JComboBox<AudioDevices.Device> inputCombo;
private final JComboBox<AudioDevices.Device> outputCombo;
private final JSlider inputGain;
private final JSlider outputVol;
private final JCheckBox denoiseCheck;
private final JSlider denoiseLevel;
private final JCheckBox typingCheck;
private final JCheckBox agcCheck;
DevicesPanel(Settings settings, VoiceOutput livePlayback,
Consumer<Consumer<VoiceInput>> applyLive,
Runnable onInputDeviceChanged, Consumer<String> onOutputDeviceChanged) {
GridBagConstraints c = gbc();
List<AudioDevices.Device> ins = AudioDevices.inputDevices();
List<AudioDevices.Device> outs = AudioDevices.outputDevices();
inputCombo = new JComboBox<>(ins.toArray(new AudioDevices.Device[0]));
outputCombo = new JComboBox<>(outs.toArray(new AudioDevices.Device[0]));
selectOrDefault(inputCombo, settings.inputDevice);
selectOrDefault(outputCombo, settings.outputDevice);
String deviceHint = "<html>Named devices are PipeWire's, and are routed through it "
+ "(so per-application volume and rerouting keep working).<br>"
+ "<b>ALSA:</b> entries talk to the sound card directly, taking it exclusively.</html>";
inputCombo.setToolTipText(deviceHint);
outputCombo.setToolTipText(deviceHint);
limitWidth(inputCombo, FIELD_WIDTH);
limitWidth(outputCombo, FIELD_WIDTH);
int row = 0;
addRow(this, c, row++, new JLabel("Capture device (microphone):"), inputCombo);
addRow(this, c, row++, new JLabel("Playback device (speakers):"), outputCombo);
inputGain = new JSlider(0, 200, (int) Math.round(settings.inputVolume * 100));
outputVol = new JSlider(0, 200, (int) Math.round(settings.outputVolume * 100));
limitWidth(inputGain, SLIDER_WIDTH);
limitWidth(outputVol, SLIDER_WIDTH);
addRow(this, c, row++, new JLabel("Microphone gain:"), inputGain);
addRow(this, c, row++, new JLabel("Playback volume:"), outputVol);
outputVol.addChangeListener(e -> {
if (livePlayback != null) livePlayback.setMasterVolume(outputVol.getValue() / 100.0);
});
inputGain.addChangeListener(e ->
applyLive.accept(m -> m.setInputGain(inputGain.getValue() / 100.0)));
inputCombo.addActionListener(e -> onInputDeviceChanged.run());
outputCombo.addActionListener(e -> onOutputDeviceChanged.accept(comboValue(outputCombo)));
c.gridx = 0;
c.gridy = row++;
c.gridwidth = 2;
c.insets = new Insets(14, 4, 2, 4);
add(new JLabel("Noise reduction"), c);
c.insets = new Insets(4, 4, 4, 4);
c.gridwidth = 1;
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));
limitWidth(denoiseLevel, SLIDER_WIDTH);
denoiseLevel.setToolTipText("Higher = more aggressive noise removal.");
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>");
agcCheck = new JCheckBox("Automatic gain control (AGC)", settings.agc);
agcCheck.setToolTipText("<html><b>Automatic gain control</b> normalises your "
+ "microphone loudness to a target level, boosting quiet mics and taming "
+ "loud ones.</html>");
c.gridx = 0;
c.gridy = row++;
c.gridwidth = 2;
add(denoiseCheck, c);
c.gridwidth = 1;
addRow(this, c, row++, new JLabel("Noise removal level:"), denoiseLevel);
c.gridx = 0;
c.gridy = row++;
c.gridwidth = 2;
add(typingCheck, c);
c.gridy = row++;
add(agcCheck, c);
c.gridwidth = 1;
Runnable syncNoise = () -> {
denoiseLevel.setEnabled(denoiseCheck.isSelected());
applyLive.accept(m -> {
m.setNoiseSuppression(denoiseCheck.isSelected());
m.setDenoiserLevel(denoiseLevel.getValue() / 100.0);
m.setTypingAttenuation(typingCheck.isSelected());
m.setAgc(agcCheck.isSelected());
});
};
denoiseCheck.addActionListener(e -> syncNoise.run());
typingCheck.addActionListener(e -> syncNoise.run());
agcCheck.addActionListener(e -> syncNoise.run());
denoiseLevel.addChangeListener(e ->
applyLive.accept(m -> m.setDenoiserLevel(denoiseLevel.getValue() / 100.0)));
syncNoise.run();
c.gridx = 0;
c.gridy = row;
c.weighty = 1;
add(Box.createGlue(), c);
}
/** Copies the form into {@code target}, without touching anything else. */
void writeInto(Settings target) {
target.inputDevice = comboValue(inputCombo);
target.outputDevice = comboValue(outputCombo);
target.inputVolume = inputGain.getValue() / 100.0;
target.outputVolume = outputVol.getValue() / 100.0;
target.denoise = denoiseCheck.isSelected();
target.denoiserLevel = denoiseLevel.getValue() / 100.0;
target.typingAttenuation = typingCheck.isSelected();
target.agc = agcCheck.isSelected();
}
private static void selectOrDefault(JComboBox<AudioDevices.Device> combo, String deviceId) {
if (deviceId != null && !deviceId.isEmpty()) {
for (int i = 0; i < combo.getItemCount(); i++) {
if (deviceId.equals(combo.getItemAt(i).id())) {
combo.setSelectedIndex(i);
return;
}
}
}
combo.setSelectedIndex(0);
}
private static String comboValue(JComboBox<AudioDevices.Device> combo) {
AudioDevices.Device d = (AudioDevices.Device) combo.getSelectedItem();
return d == null ? "" : d.id();
}
}