Play sound-pack notifications for client actions

Adds TeamSpeak-format sound packs: a folder of waves plus a settings.ini
mapping actions to play()/say() entries, with ${clientType} and friends
resolved per event. Packs are found in the client's own sound folder, an
installed TS3 client and a folder of the user's choosing, so the official
packs work unchanged.

Each action can be switched off or marked important; important actions are
the only ones still played while the speakers are muted, as in TS3. The new
Notifications options page lists them by category, greys out what the active
pack has no sound for, and previews on double-click.

Sounds are decoded, resampled and mixed onto a single playback line that is
only open while something plays, so overlapping events never fight over the
device.

Fires the events from the protocol layer, following TeamSpeak's own
distinctions: reason ids separate switched/moved/kicked/banned/timed out,
and visibility decides appears/disappears/stays.

Also fixes a ts3j trap in the process: a field an event never carried reads
back as an empty string, so the existing "e.get(x) != null" checks were
always true. That made a partial clientupdate (someone muting) announce a
stopped recording, and it let a nickname-only update reset another client's
mute/away flags and talk power, or a channel edit blank the channel name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 11:53:11 +00:00
parent 23896456ee
commit 2d7b82f9a3
23 changed files with 2103 additions and 32 deletions

View File

@@ -6,6 +6,8 @@ import com.ts3client.config.Bookmark;
import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings;
import com.ts3client.sound.SoundNotifier;
import com.ts3client.sound.SoundPlayer;
import javax.swing.BorderFactory;
import javax.swing.Box;
@@ -50,6 +52,9 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private final Bookmarks bookmarks = Bookmarks.load();
private final IdentityStore identities;
private final AudioBackend audio = new DesktopAudioBackend();
/** Sound pack playback, shared by every connection. */
private final SoundNotifier sounds;
private final SoundPlayer soundPlayer;
private final List<ServerTab> tabs = new ArrayList<>();
private final ServerTabPane tabPane = new ServerTabPane(this);
@@ -83,6 +88,9 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
super("TS3J — TeamSpeak 3 Java Client");
this.settings = settings;
this.identities = IdentityStore.load(settings);
this.sounds = new SoundNotifier(settings);
this.soundPlayer = audio.createSoundPlayer(settings);
this.sounds.setPlayer(soundPlayer);
setIconImage(Icons.app().getImage());
// We tear the connections down ourselves on close, so don't let Swing kill the JVM.
@@ -282,7 +290,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
// ---- tab management ----
private ServerTab newTab() {
ServerTab tab = new ServerTab(this, settings, identities, audio);
ServerTab tab = new ServerTab(this, settings, identities, audio, sounds);
tabs.add(tab);
tabPane.addTab(tab);
refreshTabs();
@@ -505,6 +513,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
for (ServerTab tab : new ArrayList<>(tabs)) {
tab.shutdown();
}
soundPlayer.shutdown();
}
private void showIdentities() {
@@ -517,12 +526,14 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
SettingsDialog dlg = new SettingsDialog(this, settings,
micTab == null ? null : micTab.connection().getMicrophone(),
selected == null ? null : selected.connection().getPlayback(),
sounds,
this::applyOutputSettingsToAllTabs);
dlg.setVisible(true);
}
/** Master volume / output device are global, so push them to every open connection. */
private void applyOutputSettingsToAllTabs() {
soundPlayer.setOutputDevice(settings.outputDevice);
for (ServerTab tab : tabs) {
if (tab.connection().getPlayback() == null) continue;
tab.connection().getPlayback().setMasterVolume(settings.outputVolume);