Initial commit: TS3J TeamSpeak 3 Java client
Swing desktop client (core/desktop/swing Maven modules) built on the ts3j protocol library, included as a submodule. Native Opus voice with voice-activation detection, push-to-talk, and audio pre-processing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.audio.OpusParameters;
|
||||
import com.ts3client.audio.VoiceInput;
|
||||
import com.ts3client.audio.VoiceOutput;
|
||||
import com.ts3client.audio.desktop.AudioDevices;
|
||||
import com.ts3client.config.Settings;
|
||||
|
||||
import javax.sound.sampled.TargetDataLine;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JRadioButton;
|
||||
import javax.swing.JSlider;
|
||||
import javax.swing.JTabbedPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Frame;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Options dialog: audio device selection plus voice-activation / push-to-talk
|
||||
* tuning with a live input meter. Changes are applied to the running audio
|
||||
* subsystem immediately and persisted to {@link Settings} on OK.
|
||||
*/
|
||||
public final class SettingsDialog extends JDialog {
|
||||
|
||||
private final Settings settings;
|
||||
private final VoiceInput liveMic;
|
||||
private final VoiceOutput livePlayback;
|
||||
private final Runnable onApply;
|
||||
|
||||
private JComboBox<String> inputCombo;
|
||||
private JComboBox<String> outputCombo;
|
||||
private JSlider inputGain;
|
||||
private JSlider outputVol;
|
||||
private JCheckBox denoiseCheck;
|
||||
private JSlider denoiseLevel;
|
||||
private JCheckBox typingCheck;
|
||||
private JCheckBox agcCheck;
|
||||
|
||||
private JRadioButton vadRadio;
|
||||
private JRadioButton pttRadio;
|
||||
private JRadioButton contRadio;
|
||||
private JComboBox<String> vadModeCombo;
|
||||
private JSlider thresholdSlider;
|
||||
private JSlider speechSlider;
|
||||
private JCheckBox vadOverPttCheck;
|
||||
private JLabel thresholdLabel;
|
||||
private JLabel speechLabel;
|
||||
private LevelMeter meter;
|
||||
private JButton pttKeyButton;
|
||||
private int pttKey;
|
||||
private JSlider bitrateSlider;
|
||||
private JLabel bitrateLabel;
|
||||
private JSlider complexitySlider;
|
||||
private JCheckBox vbrCheck;
|
||||
private JCheckBox fecCheck;
|
||||
private JCheckBox musicCheck;
|
||||
|
||||
private volatile boolean meterRunning;
|
||||
private Thread meterThread;
|
||||
|
||||
public SettingsDialog(Frame owner, Settings settings,
|
||||
VoiceInput liveMic, VoiceOutput livePlayback,
|
||||
Runnable onApply) {
|
||||
super(owner, "Options", true);
|
||||
this.settings = settings;
|
||||
this.liveMic = liveMic;
|
||||
this.livePlayback = livePlayback;
|
||||
this.onApply = onApply;
|
||||
this.pttKey = settings.pushToTalkKey;
|
||||
|
||||
JTabbedPane tabs = new JTabbedPane();
|
||||
tabs.addTab("Playback / Capture", scrollable(buildDevicesTab()));
|
||||
tabs.addTab("Voice Activation", scrollable(buildVoiceTab()));
|
||||
|
||||
JPanel buttons = new JPanel(new BorderLayout());
|
||||
JPanel right = new JPanel();
|
||||
JButton ok = new JButton("OK");
|
||||
JButton cancel = new JButton("Cancel");
|
||||
ok.addActionListener(e -> {
|
||||
apply();
|
||||
close();
|
||||
});
|
||||
cancel.addActionListener(e -> close());
|
||||
right.add(ok);
|
||||
right.add(cancel);
|
||||
buttons.add(right, BorderLayout.EAST);
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(tabs, BorderLayout.CENTER);
|
||||
getContentPane().add(buttons, BorderLayout.SOUTH);
|
||||
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(java.awt.event.WindowEvent e) {
|
||||
stopMeter();
|
||||
}
|
||||
});
|
||||
|
||||
pack();
|
||||
setSize(new Dimension(480, 540));
|
||||
setLocationRelativeTo(owner);
|
||||
startMeter();
|
||||
}
|
||||
|
||||
private JPanel buildDevicesTab() {
|
||||
JPanel p = new JPanel(new GridBagLayout());
|
||||
p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
GridBagConstraints c = gbc();
|
||||
|
||||
List<String> ins = AudioDevices.inputDeviceNames();
|
||||
List<String> outs = AudioDevices.outputDeviceNames();
|
||||
ins.add(0, "(System default)");
|
||||
outs.add(0, "(System default)");
|
||||
|
||||
inputCombo = new JComboBox<>(ins.toArray(new String[0]));
|
||||
outputCombo = new JComboBox<>(outs.toArray(new String[0]));
|
||||
selectOrDefault(inputCombo, settings.inputDevice);
|
||||
selectOrDefault(outputCombo, settings.outputDevice);
|
||||
|
||||
int row = 0;
|
||||
addRow(p, c, row++, new JLabel("Capture device (microphone):"), inputCombo);
|
||||
addRow(p, 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));
|
||||
addRow(p, c, row++, new JLabel("Microphone gain:"), inputGain);
|
||||
addRow(p, c, row++, new JLabel("Playback volume:"), outputVol);
|
||||
|
||||
outputVol.addChangeListener(e -> {
|
||||
if (livePlayback != null) livePlayback.setMasterVolume(outputVol.getValue() / 100.0);
|
||||
});
|
||||
inputGain.addChangeListener(e -> {
|
||||
if (liveMic != null) liveMic.setInputGain(inputGain.getValue() / 100.0);
|
||||
});
|
||||
inputCombo.addActionListener(e -> restartMeter());
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = row++;
|
||||
c.gridwidth = 2;
|
||||
c.insets = new Insets(14, 4, 2, 4);
|
||||
p.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));
|
||||
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;
|
||||
p.add(denoiseCheck, c);
|
||||
c.gridwidth = 1;
|
||||
addRow(p, c, row++, new JLabel("Noise removal level:"), denoiseLevel);
|
||||
c.gridx = 0;
|
||||
c.gridy = row++;
|
||||
c.gridwidth = 2;
|
||||
p.add(typingCheck, c);
|
||||
c.gridy = row++;
|
||||
p.add(agcCheck, c);
|
||||
c.gridwidth = 1;
|
||||
|
||||
Runnable syncNoise = () -> {
|
||||
denoiseLevel.setEnabled(denoiseCheck.isSelected());
|
||||
if (liveMic != null) {
|
||||
liveMic.setNoiseSuppression(denoiseCheck.isSelected());
|
||||
liveMic.setDenoiserLevel(denoiseLevel.getValue() / 100.0);
|
||||
liveMic.setTypingAttenuation(typingCheck.isSelected());
|
||||
liveMic.setAgc(agcCheck.isSelected());
|
||||
}
|
||||
};
|
||||
denoiseCheck.addActionListener(e -> syncNoise.run());
|
||||
typingCheck.addActionListener(e -> syncNoise.run());
|
||||
agcCheck.addActionListener(e -> syncNoise.run());
|
||||
denoiseLevel.addChangeListener(e -> {
|
||||
if (liveMic != null) liveMic.setDenoiserLevel(denoiseLevel.getValue() / 100.0);
|
||||
});
|
||||
syncNoise.run();
|
||||
|
||||
// filler
|
||||
c.gridx = 0;
|
||||
c.gridy = row;
|
||||
c.weighty = 1;
|
||||
p.add(Box.createGlue(), c);
|
||||
return p;
|
||||
}
|
||||
|
||||
private JPanel buildVoiceTab() {
|
||||
JPanel p = new JPanel(new GridBagLayout());
|
||||
p.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
GridBagConstraints c = gbc();
|
||||
|
||||
vadRadio = new JRadioButton("Voice Activation Detection");
|
||||
pttRadio = new JRadioButton("Push-To-Talk");
|
||||
contRadio = new JRadioButton("Continuous");
|
||||
ButtonGroup group = new ButtonGroup();
|
||||
group.add(vadRadio);
|
||||
group.add(pttRadio);
|
||||
group.add(contRadio);
|
||||
switch (settings.inputMode) {
|
||||
case PUSH_TO_TALK:
|
||||
pttRadio.setSelected(true);
|
||||
break;
|
||||
case CONTINUOUS:
|
||||
contRadio.setSelected(true);
|
||||
break;
|
||||
default:
|
||||
vadRadio.setSelected(true);
|
||||
}
|
||||
|
||||
int row = 0;
|
||||
c.gridx = 0;
|
||||
c.gridy = row++;
|
||||
c.gridwidth = 2;
|
||||
p.add(vadRadio, c);
|
||||
c.gridy = row++;
|
||||
p.add(pttRadio, c);
|
||||
c.gridy = row++;
|
||||
p.add(contRadio, c);
|
||||
c.gridwidth = 1;
|
||||
|
||||
meter = new LevelMeter();
|
||||
meter.setThreshold(settings.vadThresholdDb);
|
||||
c.gridx = 0;
|
||||
c.gridy = row;
|
||||
c.gridwidth = 2;
|
||||
c.insets = new Insets(10, 4, 2, 4);
|
||||
p.add(new JLabel("Input level (speak to test):"), c);
|
||||
c.gridy = ++row;
|
||||
p.add(meter, c);
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
c.gridwidth = 1;
|
||||
row++;
|
||||
|
||||
vadModeCombo = new JComboBox<>(new String[]{"Automatic", "Volume Gate", "Hybrid"});
|
||||
vadModeCombo.setSelectedIndex(vadModeIndex(settings.vadMode));
|
||||
vadModeCombo.setToolTipText("<html><b>Automatic</b>: intelligent speech detection.<br>"
|
||||
+ "<b>Volume Gate</b>: transmit when loud enough.<br>"
|
||||
+ "<b>Hybrid</b>: loud enough <i>and</i> detected as speech.</html>");
|
||||
addRow(p, c, row++, new JLabel("Detection:"), vadModeCombo);
|
||||
|
||||
thresholdSlider = new JSlider(-70, 0, (int) Math.round(settings.vadThresholdDb));
|
||||
thresholdLabel = new JLabel(Math.round(settings.vadThresholdDb) + " dB");
|
||||
thresholdSlider.addChangeListener(e -> {
|
||||
meter.setThreshold(thresholdSlider.getValue());
|
||||
thresholdLabel.setText(thresholdSlider.getValue() + " dB");
|
||||
if (liveMic != null) liveMic.setThresholdDb(thresholdSlider.getValue());
|
||||
});
|
||||
addRow(p, c, row++, new JLabel("Volume gate:"), sliderWithLabel(thresholdSlider, thresholdLabel));
|
||||
|
||||
speechSlider = new JSlider(0, 100, (int) Math.round(settings.speechThreshold * 100));
|
||||
speechLabel = new JLabel(Math.round(settings.speechThreshold * 100) + "%");
|
||||
speechSlider.addChangeListener(e -> {
|
||||
speechLabel.setText(speechSlider.getValue() + "%");
|
||||
if (liveMic != null) liveMic.setSpeechThreshold(speechSlider.getValue() / 100.0);
|
||||
});
|
||||
addRow(p, c, row++, new JLabel("Speech threshold:"), sliderWithLabel(speechSlider, speechLabel));
|
||||
|
||||
pttKeyButton = new JButton(keyName(pttKey));
|
||||
pttKeyButton.addActionListener(e -> capturePttKey());
|
||||
addRow(p, c, row++, new JLabel("Push-to-talk key:"), pttKeyButton);
|
||||
|
||||
vadOverPttCheck = new JCheckBox("Also detect voice while push-to-talk", settings.vadOverPtt);
|
||||
c.gridx = 0;
|
||||
c.gridy = row++;
|
||||
c.gridwidth = 2;
|
||||
p.add(vadOverPttCheck, c);
|
||||
c.gridwidth = 1;
|
||||
|
||||
bitrateSlider = new JSlider(8, 128, settings.bitrate / 1000);
|
||||
bitrateLabel = new JLabel(settings.bitrate / 1000 + " kbit/s");
|
||||
bitrateSlider.addChangeListener(e -> {
|
||||
bitrateLabel.setText(bitrateSlider.getValue() + " kbit/s");
|
||||
pushOpusLive();
|
||||
});
|
||||
JPanel brPanel = new JPanel(new BorderLayout(6, 0));
|
||||
brPanel.add(bitrateSlider, BorderLayout.CENTER);
|
||||
brPanel.add(bitrateLabel, BorderLayout.EAST);
|
||||
addRow(p, c, row++, new JLabel("Opus bitrate:"), brPanel);
|
||||
|
||||
complexitySlider = new JSlider(0, 10, settings.complexity);
|
||||
complexitySlider.addChangeListener(e -> pushOpusLive());
|
||||
addRow(p, c, row++, new JLabel("Opus complexity:"), complexitySlider);
|
||||
|
||||
vbrCheck = new JCheckBox("Variable bitrate (VBR)", settings.vbr);
|
||||
fecCheck = new JCheckBox("Forward error correction (FEC)", settings.fec);
|
||||
musicCheck = new JCheckBox("Music codec (higher fidelity)", settings.music);
|
||||
vbrCheck.addActionListener(e -> pushOpusLive());
|
||||
fecCheck.addActionListener(e -> pushOpusLive());
|
||||
musicCheck.addActionListener(e -> pushOpusLive());
|
||||
c.gridx = 0;
|
||||
c.gridy = row++;
|
||||
c.gridwidth = 2;
|
||||
p.add(vbrCheck, c);
|
||||
c.gridy = row++;
|
||||
p.add(fecCheck, c);
|
||||
c.gridy = row++;
|
||||
p.add(musicCheck, c);
|
||||
c.gridwidth = 1;
|
||||
|
||||
Runnable syncEnabled = () -> {
|
||||
boolean vad = vadRadio.isSelected();
|
||||
boolean ptt = pttRadio.isSelected();
|
||||
boolean vadContext = vad || (ptt && vadOverPttCheck.isSelected());
|
||||
Settings.VadMode vm = currentVadMode();
|
||||
boolean usesGate = vm != Settings.VadMode.AUTOMATIC;
|
||||
boolean usesSpeech = vm != Settings.VadMode.VOLUME_GATE;
|
||||
|
||||
vadModeCombo.setEnabled(vadContext);
|
||||
thresholdSlider.setEnabled(vadContext && usesGate);
|
||||
speechSlider.setEnabled(vadContext && usesSpeech);
|
||||
pttKeyButton.setEnabled(ptt);
|
||||
vadOverPttCheck.setEnabled(ptt);
|
||||
meter.setShowThreshold(vadContext && usesGate);
|
||||
|
||||
if (liveMic != null) {
|
||||
liveMic.setMode(ptt ? Settings.InputMode.PUSH_TO_TALK
|
||||
: vad ? Settings.InputMode.VOICE_ACTIVATION
|
||||
: Settings.InputMode.CONTINUOUS);
|
||||
liveMic.setVadMode(vm);
|
||||
liveMic.setVadOverPtt(vadOverPttCheck.isSelected());
|
||||
}
|
||||
};
|
||||
vadRadio.addActionListener(e -> syncEnabled.run());
|
||||
pttRadio.addActionListener(e -> syncEnabled.run());
|
||||
contRadio.addActionListener(e -> syncEnabled.run());
|
||||
vadModeCombo.addActionListener(e -> syncEnabled.run());
|
||||
vadOverPttCheck.addActionListener(e -> syncEnabled.run());
|
||||
syncEnabled.run();
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = row;
|
||||
c.weighty = 1;
|
||||
p.add(Box.createGlue(), c);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static int vadModeIndex(Settings.VadMode m) {
|
||||
switch (m) {
|
||||
case AUTOMATIC:
|
||||
return 0;
|
||||
case VOLUME_GATE:
|
||||
return 1;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
private Settings.VadMode currentVadMode() {
|
||||
switch (vadModeCombo.getSelectedIndex()) {
|
||||
case 0:
|
||||
return Settings.VadMode.AUTOMATIC;
|
||||
case 1:
|
||||
return Settings.VadMode.VOLUME_GATE;
|
||||
default:
|
||||
return Settings.VadMode.HYBRID;
|
||||
}
|
||||
}
|
||||
|
||||
private static javax.swing.JScrollPane scrollable(JPanel content) {
|
||||
javax.swing.JScrollPane sp = new javax.swing.JScrollPane(content,
|
||||
javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
|
||||
javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
sp.setBorder(null);
|
||||
sp.getVerticalScrollBar().setUnitIncrement(16);
|
||||
return sp;
|
||||
}
|
||||
|
||||
private static JPanel sliderWithLabel(JSlider slider, JLabel valueLabel) {
|
||||
JPanel panel = new JPanel(new BorderLayout(6, 0));
|
||||
panel.add(slider, BorderLayout.CENTER);
|
||||
valueLabel.setPreferredSize(new Dimension(48, valueLabel.getPreferredSize().height));
|
||||
panel.add(valueLabel, BorderLayout.EAST);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private OpusParameters currentOpusParameters() {
|
||||
return new OpusParameters(
|
||||
bitrateSlider.getValue() * 1000,
|
||||
complexitySlider.getValue(),
|
||||
vbrCheck.isSelected(),
|
||||
fecCheck.isSelected(),
|
||||
settings.packetLoss,
|
||||
musicCheck.isSelected());
|
||||
}
|
||||
|
||||
/** Applies the current Opus controls to the running encoder immediately. */
|
||||
private void pushOpusLive() {
|
||||
if (liveMic != null) liveMic.setOpusParameters(currentOpusParameters());
|
||||
}
|
||||
|
||||
private void capturePttKey() {
|
||||
pttKeyButton.setText("Press a key…");
|
||||
pttKeyButton.requestFocusInWindow();
|
||||
KeyAdapter ka = new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
pttKey = e.getKeyCode();
|
||||
pttKeyButton.setText(keyName(pttKey));
|
||||
pttKeyButton.removeKeyListener(this);
|
||||
}
|
||||
};
|
||||
pttKeyButton.addKeyListener(ka);
|
||||
}
|
||||
|
||||
private static String keyName(int code) {
|
||||
String t = KeyEvent.getKeyText(code);
|
||||
return (t == null || t.isEmpty()) ? ("Key " + code) : t;
|
||||
}
|
||||
|
||||
private void apply() {
|
||||
settings.inputDevice = comboValue(inputCombo);
|
||||
settings.outputDevice = comboValue(outputCombo);
|
||||
settings.inputVolume = inputGain.getValue() / 100.0;
|
||||
settings.outputVolume = outputVol.getValue() / 100.0;
|
||||
settings.denoise = denoiseCheck.isSelected();
|
||||
settings.denoiserLevel = denoiseLevel.getValue() / 100.0;
|
||||
settings.typingAttenuation = typingCheck.isSelected();
|
||||
settings.agc = agcCheck.isSelected();
|
||||
settings.inputMode = pttRadio.isSelected() ? Settings.InputMode.PUSH_TO_TALK
|
||||
: contRadio.isSelected() ? Settings.InputMode.CONTINUOUS
|
||||
: Settings.InputMode.VOICE_ACTIVATION;
|
||||
settings.vadMode = currentVadMode();
|
||||
settings.vadThresholdDb = thresholdSlider.getValue();
|
||||
settings.speechThreshold = speechSlider.getValue() / 100.0;
|
||||
settings.vadOverPtt = vadOverPttCheck.isSelected();
|
||||
settings.pushToTalkKey = pttKey;
|
||||
settings.bitrate = bitrateSlider.getValue() * 1000;
|
||||
settings.complexity = complexitySlider.getValue();
|
||||
settings.vbr = vbrCheck.isSelected();
|
||||
settings.fec = fecCheck.isSelected();
|
||||
settings.music = musicCheck.isSelected();
|
||||
settings.save();
|
||||
|
||||
if (liveMic != null) {
|
||||
liveMic.setMode(settings.inputMode);
|
||||
liveMic.setVadMode(settings.vadMode);
|
||||
liveMic.setThresholdDb(settings.vadThresholdDb);
|
||||
liveMic.setSpeechThreshold(settings.speechThreshold);
|
||||
liveMic.setVadOverPtt(settings.vadOverPtt);
|
||||
liveMic.setInputGain(settings.inputVolume);
|
||||
liveMic.setNoiseSuppression(settings.denoise);
|
||||
liveMic.setDenoiserLevel(settings.denoiserLevel);
|
||||
liveMic.setTypingAttenuation(settings.typingAttenuation);
|
||||
liveMic.setAgc(settings.agc);
|
||||
liveMic.setOpusParameters(OpusParameters.from(settings));
|
||||
}
|
||||
if (livePlayback != null) {
|
||||
livePlayback.setMasterVolume(settings.outputVolume);
|
||||
livePlayback.setOutputDevice(settings.outputDevice);
|
||||
}
|
||||
if (onApply != null) onApply.run();
|
||||
}
|
||||
|
||||
private void close() {
|
||||
stopMeter();
|
||||
dispose();
|
||||
}
|
||||
|
||||
// ---- live meter ----
|
||||
|
||||
private void startMeter() {
|
||||
meterRunning = true;
|
||||
meterThread = new Thread(this::meterLoop, "settings-meter");
|
||||
meterThread.setDaemon(true);
|
||||
meterThread.start();
|
||||
}
|
||||
|
||||
private void restartMeter() {
|
||||
stopMeter();
|
||||
startMeter();
|
||||
}
|
||||
|
||||
private void stopMeter() {
|
||||
meterRunning = false;
|
||||
if (meterThread != null) {
|
||||
meterThread.interrupt();
|
||||
meterThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void meterLoop() {
|
||||
String device = comboValue(inputCombo);
|
||||
TargetDataLine line = null;
|
||||
try {
|
||||
line = AudioDevices.openCapture(device);
|
||||
line.start();
|
||||
int frame = AudioDevices.FRAME_SIZE;
|
||||
byte[] buf = new byte[frame * 2];
|
||||
double gain = (inputGain != null ? inputGain.getValue() / 100.0 : 1.0);
|
||||
while (meterRunning) {
|
||||
int read = 0;
|
||||
while (read < buf.length) {
|
||||
int n = line.read(buf, read, buf.length - read);
|
||||
if (n <= 0) break;
|
||||
read += n;
|
||||
}
|
||||
if (read < buf.length) break;
|
||||
double sumSq = 0;
|
||||
for (int i = 0; i < frame; i++) {
|
||||
short s = (short) ((buf[2 * i + 1] << 8) | (buf[2 * i] & 0xFF));
|
||||
double f = s / 32768.0 * gain;
|
||||
sumSq += f * f;
|
||||
}
|
||||
double rms = Math.sqrt(sumSq / frame);
|
||||
double db = rms <= 1e-9 ? -100 : 20 * Math.log10(rms);
|
||||
final double fdb = db;
|
||||
if (meter != null) SwingUtilities.invokeLater(() -> meter.setLevel(fdb));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
} finally {
|
||||
if (line != null) {
|
||||
try {
|
||||
line.stop();
|
||||
line.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- small helpers ----
|
||||
|
||||
private static GridBagConstraints gbc() {
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void addRow(JPanel p, GridBagConstraints c, int row, JLabel label, java.awt.Component field) {
|
||||
c.gridx = 0;
|
||||
c.gridy = row;
|
||||
c.weightx = 0;
|
||||
c.gridwidth = 1;
|
||||
p.add(label, c);
|
||||
c.gridx = 1;
|
||||
c.weightx = 1;
|
||||
p.add(field, c);
|
||||
}
|
||||
|
||||
private static void selectOrDefault(JComboBox<String> combo, String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
combo.setSelectedIndex(0);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < combo.getItemCount(); i++) {
|
||||
if (value.equals(combo.getItemAt(i))) {
|
||||
combo.setSelectedIndex(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
combo.setSelectedIndex(0);
|
||||
}
|
||||
|
||||
private static String comboValue(JComboBox<String> combo) {
|
||||
int idx = combo.getSelectedIndex();
|
||||
if (idx <= 0) return "";
|
||||
Object v = combo.getSelectedItem();
|
||||
return v == null ? "" : v.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user