package systems.brn.textvoice.client; import systems.brn.textvoice.client.audio.Speaker; import java.util.*; import java.util.concurrent.ConcurrentHashMap; public class PlayerAudioMixer { private final Speaker speaker; private final Map> streams = new ConcurrentHashMap<>(); private final int frameSamples = 160; private final int sampleRate = 8000; private final Map hudStates; // Master volume in 0-100 range private volatile short masterVolume = 100; public PlayerAudioMixer(Speaker speaker, Map hudStates) { this.speaker = speaker; this.hudStates = hudStates; } public void setMasterVolume(short volume) { if (volume < 0) volume = 0; if (volume > 100) volume = 100; this.masterVolume = volume; } public void enqueuePlayerFrame(String playerName, short[] pcm) { Queue queue = streams.computeIfAbsent(playerName, k -> new LinkedList<>()); synchronized (queue) { queue.add(pcm); } } public void start() { Thread thread = new Thread(() -> { short[] outputBuffer = new short[frameSamples]; // 32-bit int output long nextFrameTime = System.nanoTime(); while (true) { float[] mixBuffer = new float[frameSamples]; Arrays.fill(mixBuffer, 0.0f); for (Map.Entry> entry : streams.entrySet()) { String playerName = entry.getKey(); Queue queue = entry.getValue(); short[] frame = null; synchronized (queue) { if (!queue.isEmpty()) frame = queue.poll(); } if (frame != null) { PlayerHUDState state = hudStates.computeIfAbsent(playerName, k -> new PlayerHUDState()); float playerVolume = state.volume / 100f; // per-player volume 0.0–1.0 for (int i = 0; i < Math.min(frame.length, mixBuffer.length); i++) { mixBuffer[i] += frame[i] * playerVolume; // apply per-player volume } } } // Apply master volume and convert to 32-bit int float masterVol = masterVolume / 100f; for (int i = 0; i < frameSamples; i++) { float sample = mixBuffer[i] * masterVol; // apply master volume // clamp to 16-bit PCM range if (sample > 32767f) sample = 32767f; if (sample < -32768f) sample = -32768f; outputBuffer[i] = (short) sample; } speaker.play(outputBuffer); // Schedule next frame (20ms) nextFrameTime += (long) (frameSamples / (double) sampleRate * 1_000_000_000); long sleepTime = (nextFrameTime - System.nanoTime()) / 1_000_000; if (sleepTime > 0) { try { Thread.sleep(sleepTime); } catch (InterruptedException ignored) {} } } }, "textvoice-Mixer"); thread.setDaemon(true); thread.start(); } }