Commit Graph

101 Commits

Author SHA1 Message Date
02ff63341e Notify of pokes and private messages on the desktop
While none of the client's windows has the focus, a poke or a private
message shows a desktop notification, which the desktop's notification
centre keeps. A private conversation keeps one notification, updated as
messages come; clicking it brings the window forward with that chat open,
and they all go once a window of the client is in front again.

On Linux they go to the freedesktop.org notification service, spoken to
over the session bus by a small D-Bus client of our own on Java's Unix
sockets, so no library or native code is involved; Plasma also takes a
reply typed into the notification. Elsewhere they are AWT tray messages,
which Windows and macOS put in their notification centres. The options
can turn them off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:20:51 +00:00
a6efc0b8da Notify of pokes and private messages on Android, with TeamSpeak's sounds
While the app is out of sight, a poke or a private message posts a
notification; a message's one gathers the conversation, takes a reply
inline and opens its chat. The notifications are silent, as the sound
pack already plays TeamSpeak's own sound for either.

The build bundles the sound packs of the TeamSpeak 3 client found next
to the checkout (or at ts3.client.dir / TS3_CLIENT_DIR), unpacked into
the profile once per app version. Sound Settings picks the pack and its
volume, and speech packs talk through the phone's text-to-speech.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:12:16 +00:00
e7741e8b34 Give the Material pack every icon TeamSpeak's packs have
The rest of default_colored_2014's keys get Material counterparts:
talk power, whisper, recording, text formatting, identities, add-ons,
permissions and the like. Ping levels are drawn as signal bars, since
Material's own tell their levels apart by a hairline, and the default
group icons as lettered badges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:43:07 +00:00
cdf81f3258 Share icon packs between desktop and Android
The Android app's Material icons become a TeamSpeak-format pack shipped in
core, which the desktop offers next to the TeamSpeak packs. Android now
draws its icons from packs too and imports TeamSpeak's own zips, falling
back to the Material pack for what they lack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:39:42 +00:00
a7aa122728 Sign in to myTeamSpeak and import what the account synchronises
The myTeamSpeak client protocol and its end-to-end encryption are
reverse-engineered (docs/myteamspeak): an HTTP POST of a framed protobuf,
a PBKDF2-HMAC-SHA512 login token and account key, an AES-GCM wrapped
item key and AES-CTR items with a SHA-512 trailer.

Options on desktop and Settings on Android gain a myTeamSpeak page: sign
in and out, and the account's bookmarks and identities, each picked for
import and marked when already here. The client stays signed in the way
the official one does, keeping the derived token and key rather than the
password; each read signs in afresh, and a sign-in the server no longer
takes signs out. Nothing is written to the account.

Items are the same Item_Data as the local TeamSpeak store, so they go
through the existing decoder and importer; a bookmark chosen alone brings
the identity it connects with, found by UUID or, as some name it, by
the identity's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:07:17 +00:00
73a15a8cfc Let AGC raise the noise floor further to bring up a distant voice
AGC2 won't amplify the background noise above -50 dBFS, which also caps
the gain for a voice barely above that noise: a distant talker ended up
5 dB quieter than a near one with noise suppression on, 12 dB with it
off. A "Boost quiet speech" setting (0-20 dB, default 0) raises the cap,
live, on desktop and Android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:08:50 +00:00
2281c125fa Keep the error id a server refused the last connect with
The message alone does not tell a ban from a refusal worth retrying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:30:23 +00:00
25ca586a50 Let a disconnect give up a connect still under way
Disconnecting only acted once connected, so a connect that was stuck or
retrying could not be called off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:30:23 +00:00
ed526897dc Switch audio devices without reopening the connection
A new input device used to wait for the next connect, and the shared mixer
kept its line on the old output until it went idle for good. The microphone
now reopens on the new device as the settings are applied, and the mixer
moves its line over when the output device changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:30:23 +00:00
70c6eb769c Say why a connection ended, and mark the channel we are in subscribed
- A failed connect keeps its reason in the status instead of a bare
  "Disconnected", as does a disconnect the server gave a reason for.
- The server never announces the subscription to the channel we start
  out in, although it keeps us subscribed after we leave it, so the
  channel we enter counts as subscribed.
- A client connecting or leaving the server is logged as that, not as
  switching from or to "channel #0": the server gives both the same
  reason, only with no channel on one side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:12:50 +00:00
d83ba1145b Make a parsed spacer a value
So a frontend comparing snapshots of the tree sees an unchanged spacer
as unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:21:51 +00:00
35d6427456 Apply voice settings to a live microphone in one place
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:10:35 +00:00
d206b12a30 Move spacers, client status and chat entries into core
The Android client needs them too: which state a client's icon shows,
how spacer channels are named, and the HTML a chat line is written as.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:09:43 +00:00
c5a49fc7b3 Avoid JDK APIs newer than Android's minSdk 33 in core
Files.readString/writeString (API 36.1), Path.of, ByteBuffer.slice(int, int)
and Matcher.appendReplacement(StringBuilder) (API 34) threw NoSuchMethodError
on Android; connecting crashed in ChatLogStore. Each replacement keeps the
original desktop code in a comment above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:05:39 +00:00
71ac2c817c Test the channel tree, the event handler and settings
- the tree's sibling chain: arrival order, broken links, removal,
  reordering and reparenting, orphans, client order, path lookup
- the event handler, fed events parsed from the server's own wording:
  joins, moves, leaves, partial and clearing updates, server groups,
  and a channel's life from creation to deletion
- settings: saving and loading back, unknown values, and the one-time
  conversion of the old dBFS voice threshold

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
bf07f0bf49 Move what the servers share out of the main window
Which server captures with the microphone, handing it over, push-to-talk
and key presses reaching the right microphones, and away state and
nickname applied everywhere were all worked out in MainFrame. They move
to a Sessions class in core, next to ServerSession, where another
frontend can use them; the window keeps only the tabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
ae8d1f7083 Fail a download whose file server stops sending
The data socket had no read timeout, so a file server that went quiet
left the transfer, and its thread, waiting until the user cancelled.
Thirty seconds without data now fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
48a0032848 Parse every enum setting with the one generic parser
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
3cdade9ad7 Keep the profile private and write it all at once
Settings, bookmarks (server passwords) and identities (private keys)
were written in place with the default permissions, readable by every
user on the machine, and a crash mid-write left a truncated file that
the next start silently replaced with defaults.

Every profile file is now written to a private temporary file, synced
and moved over the old one in one step. The profile folder itself is
made accessible to its owner only, which also covers files written
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
d7261c906d Let chat logs be turned off or kept elsewhere
Chat logs always went into the TeamSpeak 3 client's own folder. A new
Chat page in Options can turn them off, or point them at another
folder; left empty, they are still shared with the TeamSpeak 3 client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
1a3a3e37c1 Write both kinds of chat message through one method
appendMessage and appendPrivateMessage built the same entry twice; they
now differ only in which log they pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
a5c4ba1eb5 Load chat history from the end of the log only
Chat logs grow forever, and every connect and every private chat opened
read the whole log to keep its last 1000 entries. History now reads the
last 4 MiB, which holds far more than that; searching still reads the
whole log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
f9b2f4d7cb Keep every line of a multi-line message in chat history
The text log has a message's line breaks in it, but history was read one
line at a time, so every line after the first failed to parse and was
dropped. A line that starts no entry of its own now continues the
message before it, which also recovers what earlier versions wrote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
8e9c461424 Wait for the capture loop when the microphone stops
stop() closed the encoder and cleared the transmit state while the
capture loop could still be in its last frame, which could turn the
transmit indicator back on, or overlap the loop of a quick restart on
the same speech detector and processing chain. It now closes the line
to wake the loop and waits for it to finish before tearing down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
12ca585d90 Open a new line when a speaker's playback line dies
Both desktop backends dropped writes to a line whose device had gone
away, and the per-speaker playout ignored any failure, so that speaker
stayed silent for the rest of the session. A write to a dead line now
throws; the playout drops that line and the speaker's next packet
opens a fresh one. Sound effects and the microphone test's loopback
reopen their line the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
6234ec6d97 Give back a speaker's line after 30 s of silence
Every client that ever spoke kept an open playback line and a thread
until it left the server. A speaker silent for 30 s now has both
released, and its next talk burst opens them again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
0083dc8ad6 Play incoming voice through TeamSpeak's adaptive jitter buffer
Each speaker's stream held a fixed 60 ms and gave up after 200 ms of
concealment, so a jittery connection concealed constantly and a stall
ended the burst. It now runs through a port of the libspeex jitter
buffer the official client links, configured and driven as it does:

- the delay adapts to the measured jitter, aiming for at most 1% of
  packets arriving late
- a speaker keeps one timeline, never re-anchored between talk bursts,
  so what the buffer learned carries over; only 5 s without packets
  drops it
- the talker's stop marker never enters the buffer; playback ends when
  it reaches it, and its slot is played as silence, not concealed

A pull now always hands out one 20 ms frame: what a longer packet or a
delay increase produces beyond that is played on the following pulls,
which Android's mixer used to cut off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
7cb4f47a81 Show an avatar, description or away message being cleared
Client and channel updates were only applied when the field had a
value, since ts3j reads a missing field back as an empty string. A
removed avatar, an emptied description or a cleared away message
arrives as the field without a value, so they stayed as they were. Such
fields are now applied whenever the update carries them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
8c735a05b8 Look permissions up by name directly
selfPermissionValue scanned every permission the server lists, several
times per context menu. The model now keeps a name index next to the
id one. Also drops primaryServerGroupName, which nothing calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
67f8f46cab Publish the channel tree's lists whole instead of refilling them
buildTree() cleared and refilled the children and client lists inside
the shared channel nodes, so a UI still walking the previous tree could
see them change under it. It now builds new lists and publishes them
unmodifiable. The node fields, written by the event and audio threads
and read by the UI, are volatile.

Whether a channel is full is now counted from the model, rather than
from a tree that may not have been rebuilt since the last join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
5b1809cc2d Hand out hotkey bindings as a snapshot
Hotkeys.all() returned its synchronised list, and every reader had to
lock it while iterating: a lock owned by one class and taken by four
others. It now returns a copy, and the lock stays inside Hotkeys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
eeccb495eb Name chat logs by the server's unique id for guests too
The id came from initserver, which never carries it, then from
serverinfo and servergetvariables, which a guest may not use; chat logs
only worked with admin rights. ts3j now derives it from the server's
handshake key, as the official client does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:35:41 +00:00
d074ffaa7a Tell permission errors apart by the server's error id
The sound for a refused action was picked by looking for "permission"
or "insufficient" in the message, which also matched errors such as an
invalid permission id. The server's error ids for insufficient client,
group and permission power say it exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:31:28 +00:00
d28bb57598 Release a previous connection's resources on connect
A connect on an instance that was still connected, or still
connecting, replaced the socket and audio pipelines without closing
them. The fields shared between the connect, event, action and UI
threads are now volatile, and read once where they are used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:30:34 +00:00
f350b19d31 Run a connection's requests on its own pool
Each action started a thread that read the socket field once it ran; a
disconnect in between cleared it, and the user saw "Could not join
channel: NullPointerException". Actions now capture the socket when
they are issued and run on a pool that lives as long as the connection.
What fails because the connection went away is no longer reported as an
error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:29:23 +00:00
ba5de5a8a8 Give the importer test a fresh profile every run
The test home under target/ outlives a run, so the identity the first
run imported made every later run fail. The test now points AppDirs at
a temporary directory of its own, which also works when another test
resolved the profile first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:27:11 +00:00
11a2a14c70 Let a session reconnect to where it was
ServerSession.reconnect() connects again to the last server with the
same identity and returns to the channel the client was in: what a
dropped connection or a changed network (Android moving from Wi-Fi to
mobile data) needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:26:03 +00:00
07aa7bd7f9 Don't report a channel join that leaving the server cut short
Connecting with a channel moves there once the channel list is in; if
the connection was left again before that, the move ran against the
closed socket and reported an error nobody could act on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:26:03 +00:00
e211ffa7b6 Share libopus' encoder controls between bindings
libopus' constants and the encoder settings built on opus_encoder_ctl
(clamping, voice/music signal) move into core, so the Android binding
reuses them rather than copying them from the desktop's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:24:12 +00:00
75d9606231 Conceal a lost voice packet with 20 ms, not 120 ms
A lost packet was concealed by decoding "nothing" into a buffer sized
for the longest Opus frame, and Opus fills whatever it is given: every
lost packet played 120 ms of made-up audio in place of 20 ms, piling up
delay on that speaker's line.

Decoders now conceal through their own call that takes the length to
make up, and the stream asks for the length of the last packet it got.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:18:06 +00:00
fecb2ba8f4 Answer what the local client may do from the model
The Swing client menu decided on its own whether a group can be
(un)assigned and whether a client's channel can be joined, and the
server groups dialog borrowed the menu's check. These are rules about
the server, not about menus, so ServerModel answers them now and any
frontend asks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:10:54 +00:00
8f385b9343 Keep each server's session state in core
What a server tab knows beyond its views - the connection lifecycle,
title and status, the identity used, the microphone/speaker/away and
commander flags, chat sending and the channel lookups - moves from the
Swing ServerTab and ServerTabSelfState into core's ServerSession, so
another frontend gets the same behaviour without copying it.

ServerSession also takes over what the Swing event bridge decided on its
own: clearing the model on disconnect, naming the tab after the server,
showing senders under their contact names, and the chat's system lines.
The bridge now only updates views. SelfState moves along, while its
icons stay with the tray.

A server switch no longer needs to queue behind the UI thread: the
disconnect is reported synchronously, so the session is free again by
the time it reconnects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:09:23 +00:00
24f4b3813d Build core for Java 17
Android will run core as well, and its runtime stops at Java 17. Pinning
the release makes the compiler reject newer language features and APIs
there, while the desktop modules keep the newest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:03:42 +00:00
94748e27ee Play notification sounds from core
The sound player was a desktop class only because it decoded wave files
through Java Sound. A small RIFF reader replaces that, so the player
moves to core and every platform gets it from AudioBackend by default.

The reader takes integer PCM up to 32 bits and 32-bit float, in plain
or extensible headers. It decodes all 151 files of the TS3 sound packs
to the same samples Java Sound does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:03:08 +00:00
7396c4ca40 Move the voice pipelines into core
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>
2026-09-25 08:01:28 +00:00
3c389de36b Resolve the client's data directories in one place
Stores read their paths from AppDirs instead of user.home, so a platform
without a home directory can point them elsewhere at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:52:32 +00:00
1d61f458ae Process the microphone like TS3, and tell it about key presses
The capture path now runs the ported WebRTC chain in place of the
home-made noise, typing and gain stages, which are removed.

- As in TS3, the speech detector judges the raw microphone signal, while
  the level meter and volume gate see the processed one.
- Noise removal takes TS3's four levels (6, 12, 18 or 21 dB); a stored
  0..1 level falls back to TS3's default of 12 dB.
- Every key press from the global input hook reaches the connected
  microphones and the microphone test, so typing attenuation engages
  while the user types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:47:04 +00:00
de5f7da236 Port TS3's WebRTC capture processing to Java
TS3 pre-processes the microphone with WebRTC's audio processing module.
AudioProcessor ports the stages it enables, in APM's order, per 10 ms at
48 kHz:

- The high-pass filter APM forces on with noise suppression, then the
  three-band split and WebRTC's noise suppressor at TS3's four levels.
- The transient suppressor behind "Typing attenuation", which only acts
  while it is told about key presses.
- AGC2 adaptive digital, WebRTC's successor to the AGC1 that TS3 runs.

The tests compare against 16-bit outputs of upstream APM builds
(webrtc-audio-processing 2.1, and 1.3 for the transient suppressor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:46:46 +00:00
ee0f15fb3c Stop tripping the server's flood protection with query commands
channelinfo, clientinfo, channellist and clientlist are query commands
that count heavily against anti-flood; clicking through the tree got
every command refused within seconds. Use what the official client does:

- Channel descriptions via channelgetdescription, whose answer arrives as
  an invoker-less channel edit.
- Client version and platform via clientgetvariables, answered as a
  client update.
- Connection info takes version and platform from the model and idle
  time from notifyconnectioninfo instead of polling clientinfo.
- Drop the channellist/clientlist re-read after connecting: the events
  during connect already carry everything, and more (groups, unique ids,
  away state).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 18:32:05 +00:00
a4057cd499 Add text queries and whole-log chat search
TextQuery holds a find bar's text and options (match case, whole word,
regex) and matches plain text; HtmlText strips a log entry down to it.
ChatLogStore can now search a whole log rather than only the history
loaded into a chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 15:09:45 +00:00