Capture processing, the jitter buffer and decoding were desktop classes, so any other platform would have had to copy them. They now live in core and reach the platform only through two interfaces: - AudioIo lists devices and opens capture/playback lines; the desktop's AudioDevices implements it over PipeWire and Java Sound. - OpusCodec creates encoders and decoders; the desktop binds libopus through the FFM API as before. DesktopVoiceInput becomes CaptureVoiceInput unchanged in behaviour. DesktopVoiceOutput splits into VoiceStream, one speaker's jitter buffer and decoder, paced by whoever pulls it, and StreamingVoiceOutput around it. Speakers reach the device either on a line each (the desktop, so each shows up in the PipeWire mixer) or mixed onto one shared line, as mobile audio APIs want. The settings dialog's device lists and microphone test now go through the AudioBackend instead of desktop classes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
201 lines
7.8 KiB
Java
201 lines
7.8 KiB
Java
package com.ts3client.ui;
|
|
|
|
import com.formdev.flatlaf.util.UIScale;
|
|
import com.ts3client.audio.AudioBackend;
|
|
import com.ts3client.audio.OpusParameters;
|
|
import com.ts3client.audio.VoiceInput;
|
|
import com.ts3client.audio.VoiceOutput;
|
|
import com.ts3client.config.Settings;
|
|
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;
|
|
import java.awt.Frame;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* Options dialog: a tabbed coordinator over the individual settings pages. Each tab owns
|
|
* its own controls and reads/writes {@link Settings} on {@link #apply()}; this class wires
|
|
* the tabs that preview themselves live against the running audio subsystem (the device
|
|
* and voice-activation tabs, which share a single microphone test) and owns the
|
|
* OK/Cancel/Apply plumbing.
|
|
*/
|
|
public final class SettingsDialog extends JDialog {
|
|
|
|
private final Settings settings;
|
|
private final VoiceInput liveMic;
|
|
private final VoiceOutput livePlayback;
|
|
private final Runnable onApply;
|
|
|
|
private final DevicesPanel devicesPanel;
|
|
private final VoiceActivationPanel voiceActivationPanel;
|
|
private final NotificationsPanel notificationsPanel;
|
|
private final DesignPanel designPanel;
|
|
private final HotkeysPanel hotkeysPanel;
|
|
private final ClientVersionPanel clientVersionPanel;
|
|
private final ContactsPanel contactsPanel;
|
|
|
|
public SettingsDialog(Frame owner, Settings settings, AudioBackend audio,
|
|
VoiceInput liveMic, VoiceOutput livePlayback,
|
|
SoundNotifier sounds, HotkeyService hotkeys, Runnable onApply) {
|
|
super(owner, "Options", true);
|
|
this.settings = settings;
|
|
this.liveMic = liveMic;
|
|
this.livePlayback = livePlayback;
|
|
this.onApply = onApply;
|
|
|
|
notificationsPanel = new NotificationsPanel(settings, sounds);
|
|
designPanel = new DesignPanel(settings);
|
|
hotkeysPanel = new HotkeysPanel(hotkeys);
|
|
clientVersionPanel = new ClientVersionPanel(settings);
|
|
contactsPanel = new ContactsPanel(settings);
|
|
devicesPanel = new DevicesPanel(settings, audio.io(), livePlayback,
|
|
this::applyLive, this::restartTest, this::setTestOutputDevice);
|
|
voiceActivationPanel = new VoiceActivationPanel(settings, audio, liveMic, hotkeys,
|
|
hotkeysPanel, this::audioSnapshot);
|
|
|
|
JTabbedPane tabs = new JTabbedPane();
|
|
tabs.addTab("Playback / Capture", scrollable(devicesPanel));
|
|
tabs.addTab("Voice Activation", scrollable(voiceActivationPanel));
|
|
tabs.addTab("Notifications", notificationsPanel);
|
|
tabs.addTab("Design", designPanel);
|
|
tabs.addTab("Hotkeys", hotkeysPanel);
|
|
tabs.addTab("Contacts", contactsPanel);
|
|
tabs.addTab("Client Version", scrollable(clientVersionPanel));
|
|
|
|
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 -> cancel());
|
|
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);
|
|
|
|
// 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();
|
|
}
|
|
});
|
|
|
|
pack();
|
|
setSize(UIScale.scale(new Dimension(480, 540)));
|
|
setMinimumSize(UIScale.scale(new Dimension(420, 360)));
|
|
setLocationRelativeTo(owner);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/** Copies the audio tabs into {@code target}, without touching anything else. */
|
|
private void writeAudioSettings(Settings target) {
|
|
devicesPanel.writeInto(target);
|
|
voiceActivationPanel.writeInto(target);
|
|
}
|
|
|
|
/**
|
|
* The audio tabs as a standalone {@link Settings}, so the microphone test chain runs
|
|
* with the values currently on screen rather than the ones last saved.
|
|
*
|
|
* <p>The test always runs voice activation: it exists to tune the gate, and push-to-talk
|
|
* would need the global hotkey, which belongs to the connected microphone.
|
|
*/
|
|
private Settings audioSnapshot() {
|
|
Settings snapshot = new Settings();
|
|
writeAudioSettings(snapshot);
|
|
if (snapshot.inputMode == Settings.InputMode.PUSH_TO_TALK) {
|
|
snapshot.inputMode = Settings.InputMode.VOICE_ACTIVATION;
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
private void apply() {
|
|
writeAudioSettings(settings);
|
|
notificationsPanel.apply();
|
|
designPanel.apply();
|
|
clientVersionPanel.apply();
|
|
contactsPanel.apply();
|
|
hotkeysPanel.apply();
|
|
settings.save();
|
|
|
|
applyLive(m -> {
|
|
m.setMode(settings.inputMode);
|
|
m.setVadMode(settings.vadMode);
|
|
m.setThresholdDb(settings.vadThresholdDb);
|
|
m.setSpeechThreshold(settings.speechThreshold);
|
|
m.setVadOverPtt(settings.vadOverPtt);
|
|
m.setInputGain(settings.inputVolume);
|
|
m.setNoiseSuppression(settings.denoise);
|
|
m.setDenoiserLevel(settings.denoiserLevel);
|
|
m.setTypingAttenuation(settings.typingAttenuation);
|
|
m.setAgc(settings.agc);
|
|
m.setOpusParameters(OpusParameters.from(settings));
|
|
});
|
|
if (livePlayback != null) {
|
|
livePlayback.setMasterVolume(settings.outputVolume);
|
|
livePlayback.setOutputDevice(settings.outputDevice);
|
|
}
|
|
if (onApply != null) onApply.run();
|
|
}
|
|
|
|
/** Leaves without applying, putting back the settings that preview themselves live. */
|
|
private void cancel() {
|
|
notificationsPanel.revert();
|
|
designPanel.revert();
|
|
close();
|
|
}
|
|
|
|
private void close() {
|
|
voiceActivationPanel.stopTest();
|
|
dispose();
|
|
}
|
|
|
|
/**
|
|
* Applies a live change to the connected microphone and to the microphone test alike.
|
|
*
|
|
* <p>Guarded against a null {@code voiceActivationPanel}: the Devices tab applies its
|
|
* controls once as they are built, which happens before that tab exists.
|
|
*/
|
|
private void applyLive(Consumer<VoiceInput> change) {
|
|
if (liveMic != null) change.accept(liveMic);
|
|
if (voiceActivationPanel != null) voiceActivationPanel.configureTest(change);
|
|
}
|
|
|
|
/** Restarts the microphone test, if running, so a device change takes effect. */
|
|
private void restartTest() {
|
|
if (voiceActivationPanel != null) voiceActivationPanel.restartTest();
|
|
}
|
|
|
|
private void setTestOutputDevice(String deviceId) {
|
|
if (voiceActivationPanel != null) voiceActivationPanel.setTestOutputDevice(deviceId);
|
|
}
|
|
}
|