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 inputCombo; private final JComboBox 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> applyLive, Runnable onInputDeviceChanged, Consumer onOutputDeviceChanged) { GridBagConstraints c = gbc(); List ins = AudioDevices.inputDevices(); List 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 = "Named devices are PipeWire's, and are routed through it " + "(so per-application volume and rerouting keep working).
" + "ALSA: entries talk to the sound card directly, taking it exclusively."; 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("Typing attenuation tries to detect and " + "reduce the sounds made by typing."); agcCheck = new JCheckBox("Automatic gain control (AGC)", settings.agc); agcCheck.setToolTipText("Automatic gain control normalises your " + "microphone loudness to a target level, boosting quiet mics and taming " + "loud ones."); 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 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 combo) { AudioDevices.Device d = (AudioDevices.Device) combo.getSelectedItem(); return d == null ? "" : d.id(); } }