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>
161 lines
6.8 KiB
Java
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();
|
|
}
|
|
}
|