Add netgame voice chat

Implemented using libopus for the Opus codec, same as is used in Discord.
This adds the following cvars:

- `voice_chat` On/Off, triggers self-deafen state on server via weaponprefs
- `voice_mode` Activity/PTT
- `voice_selfmute` On/Off, triggers self-mute state on server via weaponprefs
- `voice_inputamp` -30 to 30, scales input by value in decibels
- `voice_activationthreshold` -30 to 0, if any peak in a frame is higher, activates voice
- `voice_loopback` On/Off, plays back local transcoded voice
- `voice_proximity` On/Off, enables proximity effects for server
- `voice_distanceattenuation_distance` distance in fracunits to scale voice volume over
- `voice_distanceattenuation_factor` distance in logarithmic factor to scale voice volume by distance to. e.g. 0.5 for "half as loud" at or above max distance
- `voice_stereopanning_factor` at 1.0, player voices are panned to left or right speaker, scaling to no effect at 0.0
- `voice_concurrentattenuation_factor` the logarithmic factor to attenuate player voices with concurrent speakers
- `voice_concurrentattenuation_min` the minimum concurrent speakers before global concurrent speaker attenuation
- `voice_concurrentattenuation_max` the maximum concurrent speakers for full global concurrent speaker attenuation
- `voice_servermute` whether voice chat is enabled on this server. visible from MS via bitflag
- `voicevolume` local volume of all voice playback

A Voice Options menu is added with a subset of these options, and Server Options has server mute.
This commit is contained in:
Eidolon 2024-12-13 17:12:14 -06:00
parent 6ffdeb6c44
commit 22b20b5877
40 changed files with 15772 additions and 14142 deletions

View file

@ -100,6 +100,7 @@ find_package(ZLIB REQUIRED)
find_package(PNG REQUIRED) find_package(PNG REQUIRED)
find_package(SDL2 CONFIG REQUIRED) find_package(SDL2 CONFIG REQUIRED)
find_package(CURL REQUIRED) find_package(CURL REQUIRED)
find_package(Opus REQUIRED)
# Use the one in thirdparty/fmt to guarantee a minimum version # Use the one in thirdparty/fmt to guarantee a minimum version
#find_package(FMT CONFIG REQUIRED) #find_package(FMT CONFIG REQUIRED)

View file

@ -0,0 +1,7 @@
find_package(Opus CONFIG)
if(NOT TARGET Opus::opus)
find_package(PkgConfig REQUIRED)
pkg_check_modules(Opus REQUIRED IMPORTED_TARGET opus)
set_target_properties(PkgConfig::Opus PROPERTIES IMPORTED_GLOBAL TRUE)
add_library(Opus::opus ALIAS PkgConfig::Opus)
endif()

View file

@ -258,6 +258,7 @@ endif()
target_link_libraries(SRB2SDL2 PRIVATE ZLIB::ZLIB) target_link_libraries(SRB2SDL2 PRIVATE ZLIB::ZLIB)
target_link_libraries(SRB2SDL2 PRIVATE PNG::PNG) target_link_libraries(SRB2SDL2 PRIVATE PNG::PNG)
target_link_libraries(SRB2SDL2 PRIVATE CURL::libcurl) target_link_libraries(SRB2SDL2 PRIVATE CURL::libcurl)
target_link_libraries(SRB2SDL2 PRIVATE Opus::opus)
if("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD") if("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD")
target_link_libraries(SRB2SDL2 PRIVATE -lexecinfo) target_link_libraries(SRB2SDL2 PRIVATE -lexecinfo)
target_link_libraries(SRB2SDL2 PRIVATE -lpthread) target_link_libraries(SRB2SDL2 PRIVATE -lpthread)

View file

@ -322,6 +322,7 @@ consvar_t cv_controlperkey = Player("controlperkey", "One").values({{1, "One"},
consvar_t cv_mastervolume = Player("volume", "80").min_max(0, 100); consvar_t cv_mastervolume = Player("volume", "80").min_max(0, 100);
consvar_t cv_digmusicvolume = Player("musicvolume", "80").min_max(0, 100); consvar_t cv_digmusicvolume = Player("musicvolume", "80").min_max(0, 100);
consvar_t cv_soundvolume = Player("soundvolume", "80").min_max(0, 100); consvar_t cv_soundvolume = Player("soundvolume", "80").min_max(0, 100);
consvar_t cv_voicevolume = Player("voicevolume", "100").min_max(0, 100);
#ifdef HAVE_DISCORDRPC #ifdef HAVE_DISCORDRPC
void DRPC_UpdatePresence(void); void DRPC_UpdatePresence(void);
@ -1351,8 +1352,71 @@ consvar_t cv_chatwidth = Player("chatwidth", "150").min_max(64, 150);
// old shit console chat. (mostly exists for stuff like terminal, not because I cared if anyone liked the old chat.) // old shit console chat. (mostly exists for stuff like terminal, not because I cared if anyone liked the old chat.)
consvar_t cv_consolechat = Player("chatmode", "Yes").values({{0, "Yes"}, {2, "No"}}); consvar_t cv_consolechat = Player("chatmode", "Yes").values({{0, "Yes"}, {2, "No"}});
// When off, inbound voice packets are ignored
void VoiceChat_OnChange(void);
consvar_t cv_voice_chat = Player("voice_chat", "Off")
.on_off()
.onchange(VoiceChat_OnChange)
.description("Whether voice chat is played or not. Shown as self-deafen to others.");
// When on, local player won't transmit voice
consvar_t cv_voice_mode = Player("voice_mode", "Activity")
.values({{0, "Activity"}, {1, "PTT"}})
.description("How to activate voice transmission");
consvar_t cv_voice_selfmute = Player("voice_selfmute", "Off")
.on_off()
.onchange(weaponPrefChange)
.description("Whether the local microphone is muted. Shown as self-mute to others.");
consvar_t cv_voice_inputamp = Player("voice_inputamp", "14")
.min_max(-30, 30)
.description("How much louder or quieter to make voice input, in decibels.");
consvar_t cv_voice_activationthreshold = Player("voice_activationthreshold", "-20")
.min_max(-30, 0)
.description("The voice activation threshold, in decibels from maximum amplitude.");
// When on, local voice is played back out
consvar_t cv_voice_loopback = Player("voice_loopback", "Off")
.on_off()
.dont_save()
.description("When on, plays the local player's voice");
consvar_t cv_voice_proximity = NetVar("voice_proximity", "On")
.on_off()
.description("Whether proximity effects for voice chat are enabled on the server.");
// The relative distance for maximum voice attenuation
consvar_t cv_voice_distanceattenuation_distance = NetVar("voice_distanceattenuation_distance", "4096")
.floating_point()
.description("Voice speaker's distance from listener at which positional voice is fully attenuated");
// The volume factor (scaled logarithmically, i.e. 0.5 = "half as loud") for voice distance attenuation
consvar_t cv_voice_distanceattenuation_factor = NetVar("voice_distanceattenuation_factor", "0.2")
.floating_point()
.description("Maximum attenuation, in perceived loudness, when a voice speaker is at voice_distanceattenuation_distance units or further from the listener");
// The scale factor applied to stereo separation for voice panning
consvar_t cv_voice_stereopanning_factor = NetVar("voice_stereopanning_factor", "1.0")
.floating_point()
.description("Scale of stereo panning applied to a voice speaker relative to their in-game position, from 0.0-1.0");
consvar_t cv_voice_concurrentattenuation_factor = NetVar("voice_concurrentattenuation_factor", "0.6")
.floating_point()
.description("The maximum attenuation factor, in perceived loudness, when at or exceeding voice_concurrentattenuation_max speakers");
consvar_t cv_voice_concurrentattenuation_min = NetVar("voice_concurrentattenuation_min", "3")
.description("Minimum concurrent speakers before global attenuation starts");
consvar_t cv_voice_concurrentattenuation_max = NetVar("voice_concurrentattenuation_max", "8")
.description("Maximum concurrent speakers at which full global attenuation is applied");
void Mute_OnChange(void); void Mute_OnChange(void);
void VoiceMute_OnChange(void);
consvar_t cv_mute = UnsavedNetVar("mute", "Off").on_off().onchange(Mute_OnChange); consvar_t cv_mute = UnsavedNetVar("mute", "Off").on_off().onchange(Mute_OnChange);
consvar_t cv_voice_servermute = NetVar("voice_servermute", "On")
.on_off()
.onchange(VoiceMute_OnChange)
.description("If On, the server will not broadcast voice chat to clients");
// //

View file

@ -16,6 +16,8 @@
#include <unistd.h> //for unlink #include <unistd.h> //for unlink
#endif #endif
#include <opus.h>
#include "i_time.h" #include "i_time.h"
#include "i_net.h" #include "i_net.h"
#include "i_system.h" #include "i_system.h"
@ -190,6 +192,17 @@ uint8_t priorKeys[MAXPLAYERS][PUBKEYLENGTH]; // Make a note of keys before consu
boolean serverisfull = false; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not boolean serverisfull = false; //lets us be aware if the server was full after we check files, but before downloading, so we can ask if the user still wants to download or not
tic_t firstconnectattempttime = 0; tic_t firstconnectattempttime = 0;
static OpusDecoder *g_player_opus_decoders[MAXPLAYERS];
static UINT64 g_player_opus_lastframe[MAXPLAYERS];
static OpusEncoder *g_local_opus_encoder;
static UINT64 g_local_opus_frame = 0;
#define SRB2_VOICE_OPUS_FRAME_SIZE 480
static float g_local_voice_buffer[SRB2_VOICE_OPUS_FRAME_SIZE];
static INT32 g_local_voice_buffer_len = 0;
static INT32 g_local_voice_threshold_time = 0;
float g_local_voice_last_peak = 0;
boolean g_local_voice_detected = false;
// engine // engine
// Must be a power of two // Must be a power of two
@ -1050,7 +1063,8 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
netbuffer->u.serverinfo.kartvars = (UINT8) ( netbuffer->u.serverinfo.kartvars = (UINT8) (
(gamespeed & SV_SPEEDMASK) | (gamespeed & SV_SPEEDMASK) |
(dedicated ? SV_DEDICATED : 0) (dedicated ? SV_DEDICATED : 0) |
(!cv_voice_servermute.value ? SV_VOICEENABLED : 0)
); );
D_ParseCarets(netbuffer->u.serverinfo.servername, cv_servername.string, MAXSERVERNAME); D_ParseCarets(netbuffer->u.serverinfo.servername, cv_servername.string, MAXSERVERNAME);
@ -2353,6 +2367,11 @@ static void CL_ConnectToServer(void)
} }
SL_ClearServerList(servernode); SL_ClearServerList(servernode);
for (i = 0; i < MAXPLAYERS; i++)
{
CL_ClearPlayer(i);
}
do do
{ {
// If the connection was aborted for some reason, leave // If the connection was aborted for some reason, leave
@ -2564,6 +2583,27 @@ void CL_ClearPlayer(INT32 playernum)
// Handle post-cleanup. // Handle post-cleanup.
RemoveAdminPlayer(playernum); // don't stay admin after you're gone RemoveAdminPlayer(playernum); // don't stay admin after you're gone
// Clear voice chat data
S_ResetVoiceQueue(playernum);
{
// Destroy and recreate the opus decoder for this playernum
OpusDecoder *opusdecoder = g_player_opus_decoders[playernum];
if (opusdecoder)
{
opus_decoder_destroy(opusdecoder);
opusdecoder = NULL;
}
int error;
opusdecoder = opus_decoder_create(48000, 1, &error);
if (error != OPUS_OK)
{
CONS_Alert(CONS_WARNING, "Failed to create Opus decoder for player %d: opus error %d\n", playernum, error);
opusdecoder = NULL;
}
g_player_opus_decoders[playernum] = opusdecoder;
}
} }
// //
@ -3242,9 +3282,123 @@ static void Command_ResendGamestate(void)
} }
} }
static void Command_ServerMute(void)
{
SINT8 playernum;
UINT8 buf[2];
if (!netgame)
{
CONS_Printf(M_GetText("This only works in a netgame.\n"));
return;
}
if (COM_Argc() == 1)
{
CONS_Printf(M_GetText("servermute <playername/playernum>: server mute a player's voice\n"));
return;
}
else if (!(server || IsPlayerAdmin(consoleplayer)))
{
CONS_Printf(M_GetText("Only the server or an admin can use this.\n"));
return;
}
playernum = nametonum(COM_Argv(1));
if (playernum == -1)
return;
buf[0] = playernum;
buf[1] = 1;
SendNetXCmd(XD_SERVERMUTEPLAYER, buf, 2);
}
static void Command_ServerUnmute(void)
{
SINT8 playernum;
UINT8 buf[2];
if (!netgame)
{
CONS_Printf(M_GetText("This only works in a netgame.\n"));
return;
}
if (COM_Argc() == 1)
{
CONS_Printf(M_GetText("serverunmute <playername/playernum>: server unmute a player's voice\n"));
return;
}
else if (!(server || IsPlayerAdmin(consoleplayer)))
{
CONS_Printf(M_GetText("Only the server or an admin can use this.\n"));
return;
}
playernum = nametonum(COM_Argv(1));
if (playernum == -1)
return;
buf[0] = playernum;
buf[1] = 0;
SendNetXCmd(XD_SERVERMUTEPLAYER, &buf, 2);
}
static void Command_ServerDeafen(void)
{
SINT8 playernum;
UINT8 buf[2];
if (COM_Argc() == 1)
{
CONS_Printf(M_GetText("serverdeafen <playername/playernum>: server deafen a player\n"));
return;
}
else if (client)
{
CONS_Printf(M_GetText("Only the server can use this.\n"));
return;
}
playernum = nametonum(COM_Argv(1));
if (playernum == -1)
return;
buf[0] = playernum;
buf[1] = 1;
SendNetXCmd(XD_SERVERDEAFENPLAYER, &buf, 2);
}
static void Command_ServerUndeafen(void)
{
SINT8 playernum;
UINT8 buf[2];
if (COM_Argc() == 1)
{
CONS_Printf(M_GetText("serverundeafen <playername/playernum>: server undeafen a player\n"));
return;
}
else if (client)
{
CONS_Printf(M_GetText("Only the server can use this.\n"));
return;
}
playernum = nametonum(COM_Argv(1));
if (playernum == -1)
return;
buf[0] = playernum;
buf[1] = 0;
SendNetXCmd(XD_SERVERDEAFENPLAYER, buf, 2);
}
static void Got_AddPlayer(const UINT8 **p, INT32 playernum); static void Got_AddPlayer(const UINT8 **p, INT32 playernum);
static void Got_RemovePlayer(const UINT8 **p, INT32 playernum); static void Got_RemovePlayer(const UINT8 **p, INT32 playernum);
static void Got_AddBot(const UINT8 **p, INT32 playernum); static void Got_AddBot(const UINT8 **p, INT32 playernum);
static void Got_ServerMutePlayer(const UINT8 **p, INT32 playernum);
static void Got_ServerDeafenPlayer(const UINT8 **p, INT32 playernum);
void Joinable_OnChange(void); void Joinable_OnChange(void);
void Joinable_OnChange(void) void Joinable_OnChange(void)
@ -3295,6 +3449,13 @@ void D_ClientServerInit(void)
RegisterNetXCmd(XD_REMOVEPLAYER, Got_RemovePlayer); RegisterNetXCmd(XD_REMOVEPLAYER, Got_RemovePlayer);
RegisterNetXCmd(XD_ADDBOT, Got_AddBot); RegisterNetXCmd(XD_ADDBOT, Got_AddBot);
COM_AddCommand("servermute", Command_ServerMute);
COM_AddCommand("serverunmute", Command_ServerUnmute);
COM_AddCommand("serverdeafen", Command_ServerDeafen);
COM_AddCommand("serverundeafen", Command_ServerUndeafen);
RegisterNetXCmd(XD_SERVERMUTEPLAYER, Got_ServerMutePlayer);
RegisterNetXCmd(XD_SERVERDEAFENPLAYER, Got_ServerDeafenPlayer);
gametic = 0; gametic = 0;
localgametic = 0; localgametic = 0;
@ -3484,6 +3645,27 @@ void D_QuitNetGame(void)
#endif #endif
} }
static void InitializeLocalVoiceEncoder(void)
{
// Reset voice opus encoder for local "player 1"
OpusEncoder *encoder = g_local_opus_encoder;
if (encoder != NULL)
{
opus_encoder_destroy(encoder);
encoder = NULL;
}
int error;
encoder = opus_encoder_create(48000, 1, OPUS_APPLICATION_VOIP, &error);
opus_encoder_ctl(encoder, OPUS_SET_VBR(0));
if (error != OPUS_OK)
{
CONS_Alert(CONS_WARNING, "Failed to create Opus voice encoder: opus error %d\n", error);
encoder = NULL;
}
g_local_opus_encoder = encoder;
g_local_opus_frame = 0;
}
// Adds a node to the game (player will follow at map change or at savegame....) // Adds a node to the game (player will follow at map change or at savegame....)
static inline void SV_AddNode(INT32 node) static inline void SV_AddNode(INT32 node)
{ {
@ -3563,6 +3745,8 @@ static void Got_AddPlayer(const UINT8 **p, INT32 playernum)
g_localplayers[i] = newplayernum; g_localplayers[i] = newplayernum;
} }
DEBFILE("spawning me\n"); DEBFILE("spawning me\n");
InitializeLocalVoiceEncoder();
} }
P_ForceLocalAngle(newplayer, newplayer->angleturn); P_ForceLocalAngle(newplayer, newplayer->angleturn);
@ -3662,6 +3846,56 @@ static void Got_AddBot(const UINT8 **p, INT32 playernum)
K_SetBot(newplayernum, skinnum, difficulty, style); K_SetBot(newplayernum, skinnum, difficulty, style);
} }
// Xcmd XD_SERVERMUTEPLAYER
static void Got_ServerMutePlayer(const UINT8 **p, INT32 playernum)
{
UINT8 forplayer = READUINT8(*p);
UINT8 muted = READUINT8(*p);
if (playernum != serverplayer)
{
CONS_Alert(CONS_WARNING, M_GetText("Illegal server mute player cmd from %s\n"), player_names[playernum]);
if (server)
{
SendKick(playernum, KICK_MSG_CON_FAIL);
}
}
if (muted && !(players[forplayer].pflags2 & PF2_SERVERMUTE))
{
players[forplayer].pflags2 |= PF2_SERVERMUTE;
HU_AddChatText(va("\x82* %s was server muted.", player_names[forplayer]), false);
}
else if (!muted && players[forplayer].pflags2 & PF2_SERVERMUTE)
{
players[forplayer].pflags2 &= ~PF2_SERVERMUTE;
HU_AddChatText(va("\x82* %s was server unmuted.", player_names[forplayer]), false);
}
}
// Xcmd XD_SERVERDEAFENPLAYER
static void Got_ServerDeafenPlayer(const UINT8 **p, INT32 playernum)
{
UINT8 forplayer = READUINT8(*p);
UINT8 deafened = READUINT8(*p);
if (playernum != serverplayer)
{
CONS_Alert(CONS_WARNING, M_GetText("Illegal server deafen player cmd from %s\n"), player_names[playernum]);
if (server)
{
SendKick(playernum, KICK_MSG_CON_FAIL);
}
}
if (deafened && !(players[forplayer].pflags2 & PF2_SERVERDEAFEN))
{
players[forplayer].pflags2 |= PF2_SERVERDEAFEN;
HU_AddChatText(va("\x82* %s was server deafened.", player_names[forplayer]), false);
}
else if (!deafened && players[forplayer].pflags2 & PF2_SERVERDEAFEN)
{
players[forplayer].pflags2 &= ~PF2_SERVERDEAFEN;
HU_AddChatText(va("\x82* %s was server undeafened.", player_names[forplayer]), false);
}
}
static boolean SV_AddWaitingPlayers(SINT8 node, UINT8 *availabilities, static boolean SV_AddWaitingPlayers(SINT8 node, UINT8 *availabilities,
const char *name, uint8_t *key, UINT16 *pwr, const char *name, uint8_t *key, UINT16 *pwr,
const char *name2, uint8_t *key2, UINT16 *pwr2, const char *name2, uint8_t *key2, UINT16 *pwr2,
@ -4934,6 +5168,149 @@ static void PT_ReqMapQueue(int node)
SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf); SendNetXCmd(XD_MAPQUEUE, buf, buf_p - buf);
} }
static void PT_HandleVoiceClient(SINT8 node, boolean isserver)
{
if (!isserver && node != servernode)
{
// We should never receive voice packets from anything other than the server
return;
}
if (dedicated)
{
// don't bother decoding on dedicated
return;
}
doomdata_t *pak = (doomdata_t*)(doomcom->data);
voice_pak *pl = &pak->u.voice;
UINT64 framenum = (UINT64)LONGLONG(pl->frame);
INT32 playernum = pl->flags & VOICE_PAK_FLAGS_PLAYERNUM_BITS;
if (playernum >= MAXPLAYERS || playernum < 0)
{
// ignore
return;
}
boolean terminal = (pl->flags & VOICE_PAK_FLAGS_TERMINAL_BIT) > 0;
UINT32 framesize = doomcom->datalength - BASEPACKETSIZE - sizeof(voice_pak);
UINT8 *frame = (UINT8*)(pl) + sizeof(voice_pak);
OpusDecoder *decoder = g_player_opus_decoders[playernum];
if (decoder == NULL)
{
return;
}
float *decoded_out = Z_Malloc(sizeof(float) * SRB2_VOICE_OPUS_FRAME_SIZE, PU_STATIC, NULL);
INT32 decoded_samples = 0;
UINT64 missedframes = 0;
if (framenum > g_player_opus_lastframe[playernum])
{
missedframes = min((framenum - g_player_opus_lastframe[playernum]) - 1, 16);
}
for (UINT64 i = 0; i < missedframes; i++)
{
decoded_samples = opus_decode_float(decoder, NULL, 0, decoded_out, SRB2_VOICE_OPUS_FRAME_SIZE, 0);
if (decoded_samples < 0)
{
continue;
}
if (cv_voice_chat.value != 0 && playernum != g_localplayers[0])
{
S_QueueVoiceFrameFromPlayer(playernum, (void*)decoded_out, decoded_samples * sizeof(float), false);
}
}
g_player_opus_lastframe[playernum] = framenum;
decoded_samples = opus_decode_float(decoder, frame, framesize, decoded_out, SRB2_VOICE_OPUS_FRAME_SIZE, 0);
if (decoded_samples < 0)
{
Z_Free(decoded_out);
return;
}
if (cv_voice_chat.value != 0 && playernum != g_localplayers[0])
{
S_QueueVoiceFrameFromPlayer(playernum, (void*)decoded_out, decoded_samples * sizeof(float), terminal);
}
S_SetPlayerVoiceActive(playernum);
Z_Free(decoded_out);
}
static void PT_HandleVoiceServer(SINT8 node)
{
// Relay to client nodes except the sender
doomdata_t *pak = (doomdata_t*)(doomcom->data);
voice_pak *pl = &pak->u.voice;
int playernum = -1;
player_t *player;
if (cv_voice_servermute.value != 0)
{
// Don't even relay voice packets if voice_servermute is on
return;
}
if ((pl->flags & VOICE_PAK_FLAGS_PLAYERNUM_BITS) > 0 || (pl->flags & VOICE_PAK_FLAGS_RESERVED_BITS) > 0)
{
// All bits except the terminal bit must be unset when sending to client
// Anything else is an illegal message
return;
}
playernum = nodetoplayer[node];
if (!(playernum >= 0 && playernum < MAXPLAYERS))
{
return;
}
player = &players[playernum];
if (player->pflags2 & (PF2_SELFMUTE | PF2_SELFDEAFEN | PF2_SERVERMUTE | PF2_SERVERDEAFEN))
{
// ignore, they should not be able to broadcast voice
return;
}
// Preserve terminal bit, blank all other bits
pl->flags &= VOICE_PAK_FLAGS_TERMINAL_BIT;
// Add playernum to lower bits
pl->flags |= (playernum & VOICE_PAK_FLAGS_PLAYERNUM_BITS);
for (int i = 0; i < MAXPLAYERS; i++)
{
UINT8 pnode = playernode[i];
if (pnode == UINT8_MAX)
{
continue;
}
// Is this node P1 on that node?
boolean isp1onnode = nodetoplayer[pnode] >= 0 && nodetoplayer[pnode] < MAXPLAYERS;
if (pnode != node && pnode != servernode && isp1onnode && !(players[i].pflags2 & (PF2_SELFDEAFEN | PF2_SERVERDEAFEN)))
{
HSendPacket(pnode, false, 0, doomcom->datalength - BASEPACKETSIZE);
}
}
PT_HandleVoiceClient(node, true);
}
static void PT_HandleVoice(SINT8 node)
{
if (server)
{
PT_HandleVoiceServer(node);
}
else
{
PT_HandleVoiceClient(node, false);
}
}
static char NodeToSplitPlayer(int node, int split) static char NodeToSplitPlayer(int node, int split)
{ {
if (split == 0) if (split == 0)
@ -5612,6 +5989,9 @@ static void HandlePacketFromPlayer(SINT8 node)
csprng(lastChallengeAll, sizeof(lastChallengeAll)); csprng(lastChallengeAll, sizeof(lastChallengeAll));
expectChallenge = false; expectChallenge = false;
break; break;
case PT_VOICE:
PT_HandleVoice(node);
break;
default: default:
DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n", DEBFILE(va("UNKNOWN PACKET TYPE RECEIVED %d from host %d\n",
netbuffer->packettype, node)); netbuffer->packettype, node));
@ -6759,6 +7139,9 @@ void NetKeepAlive(void)
Net_AckTicker(); Net_AckTicker();
HandleNodeTimeouts(); HandleNodeTimeouts();
FileSendTicker(); FileSendTicker();
// Update voice whenever possible.
NetVoiceUpdate();
} }
// If a tree falls in the forest but nobody is around to hear it, does it make a tic? // If a tree falls in the forest but nobody is around to hear it, does it make a tic?
@ -6946,6 +7329,138 @@ void NetUpdate(void)
FileSendTicker(); FileSendTicker();
} }
void NetVoiceUpdate(void)
{
UINT8 *encoded = NULL;
if (dedicated)
{
return;
}
// This necessarily runs every frame, not every tic
S_SoundInputSetEnabled(true);
UINT32 bytes_dequed = 0;
do
{
// We need to drain the input queue completely, so do this in a full loop
INT32 to_read = (SRB2_VOICE_OPUS_FRAME_SIZE - g_local_voice_buffer_len) * sizeof(float);
if (to_read > 0)
{
// Attempt to fill the voice frame buffer
bytes_dequed = S_SoundInputDequeueSamples((void*)(g_local_voice_buffer + g_local_voice_buffer_len), to_read);
g_local_voice_buffer_len += bytes_dequed / 4;
}
else
{
bytes_dequed = 0;
}
if (g_local_voice_buffer_len < SRB2_VOICE_OPUS_FRAME_SIZE)
{
continue;
}
// Amp of +10 dB is appromiately "twice as loud"
float ampfactor = powf(10, (float) cv_voice_inputamp.value / 20.f);
for (int i = 0; i < g_local_voice_buffer_len; i++)
{
g_local_voice_buffer[i] *= ampfactor;
}
float softmem = 0.f;
opus_pcm_soft_clip(g_local_voice_buffer, SRB2_VOICE_OPUS_FRAME_SIZE, 1, &softmem);
// Voice detection gate open/close
float maxamplitude = 0.f;
for (int i = 0; i < g_local_voice_buffer_len; i++)
{
maxamplitude = max(fabsf(g_local_voice_buffer[i]), maxamplitude);
}
// 20. * log_10(amplitude) -> decibels (up to 0)
// lower than -30 dB is usually inaudible
g_local_voice_last_peak = maxamplitude;
maxamplitude = 20.f * logf(maxamplitude);
if (maxamplitude > (float) cv_voice_activationthreshold.value)
{
g_local_voice_threshold_time = I_GetTime();
g_local_voice_detected = true;
}
switch (cv_voice_mode.value)
{
case 0:
if (I_GetTime() - g_local_voice_threshold_time > 15)
{
g_local_voice_buffer_len = 0;
g_local_voice_detected = false;
continue;
}
break;
case 1:
if (!g_voicepushtotalk_on)
{
g_local_voice_buffer_len = 0;
g_local_voice_detected = false;
continue;
}
g_local_voice_detected = true;
break;
default:
g_local_voice_buffer_len = 0;
continue;
}
if (cv_voice_chat.value == 0)
{
g_local_voice_buffer_len = 0;
continue;
}
if (!encoded)
{
encoded = Z_Malloc(sizeof(UINT8) * 1400, PU_STATIC, NULL);
}
if (g_local_opus_encoder == NULL)
{
InitializeLocalVoiceEncoder();
}
OpusEncoder *encoder = g_local_opus_encoder;
INT32 result = opus_encode_float(encoder, g_local_voice_buffer, SRB2_VOICE_OPUS_FRAME_SIZE, encoded, 1400);
if (result < 0)
{
continue;
}
// Only send a voice packet and set local player voice active if:
// 1. In a netgame,
// 2. Not self-muted by cvar
// 3. The consoleplayer is not server or self muted or deafened
if (netgame && !cv_voice_selfmute.value && !(players[consoleplayer].pflags2 & (PF2_SERVERMUTE | PF2_SELFMUTE | PF2_SELFDEAFEN | PF2_SERVERDEAFEN)))
{
DoVoicePacket(servernode, g_local_opus_frame, encoded, result);
S_SetPlayerVoiceActive(consoleplayer);
}
if (cv_voice_loopback.value)
{
result = opus_decode_float(g_player_opus_decoders[consoleplayer], encoded, result, g_local_voice_buffer, SRB2_VOICE_OPUS_FRAME_SIZE, 0);
S_QueueVoiceFrameFromPlayer(consoleplayer, g_local_voice_buffer, result * sizeof(float), false);
}
g_local_voice_buffer_len = 0;
g_local_opus_frame += 1;
} while (bytes_dequed > 0);
if (encoded) Z_Free(encoded);
return;
}
/** Returns the number of players playing. /** Returns the number of players playing.
* \return Number of players. Can be zero if we're running a ::dedicated * \return Number of players. Can be zero if we're running a ::dedicated
* server. * server.
@ -7126,6 +7641,17 @@ void DoSayPacketFromCommand(SINT8 target, size_t usedargs, UINT8 flags)
DoSayPacket(target, flags, consoleplayer, msg); DoSayPacket(target, flags, consoleplayer, msg);
} }
void DoVoicePacket(SINT8 target, UINT64 frame, const UINT8* opusdata, size_t len)
{
voice_pak *pl = &netbuffer->u.voice;
netbuffer->packettype = PT_VOICE;
pl->frame = (UINT64)LONGLONG(frame);
pl->flags = 0;
I_Assert(MAXPACKETLENGTH - sizeof(voice_pak) - BASEPACKETSIZE >= len);
memcpy((UINT8*)netbuffer + BASEPACKETSIZE + sizeof(voice_pak), opusdata, len);
HSendPacket(target, false, 0, sizeof(voice_pak) + len);
}
// This is meant to be targeted at player indices, not whatever the hell XD_SAY is doing with 1-indexed players. // This is meant to be targeted at player indices, not whatever the hell XD_SAY is doing with 1-indexed players.
void SendServerNotice(SINT8 target, char *message) void SendServerNotice(SINT8 target, char *message)
{ {

View file

@ -137,6 +137,8 @@ typedef enum
PT_REQMAPQUEUE, // Client requesting a roundqueue operation PT_REQMAPQUEUE, // Client requesting a roundqueue operation
PT_VOICE, // Voice packet for either side
NUMPACKETTYPE NUMPACKETTYPE
} packettype_t; } packettype_t;
@ -283,6 +285,7 @@ struct clientconfig_pak
#define SV_SPEEDMASK 0x03 // used to send kartspeed #define SV_SPEEDMASK 0x03 // used to send kartspeed
#define SV_DEDICATED 0x40 // server is dedicated #define SV_DEDICATED 0x40 // server is dedicated
#define SV_VOICEENABLED 0x80 // voice_mute is off/voice chat is enabled
#define SV_LOTSOFADDONS 0x20 // flag used to ask for full file list in d_netfil #define SV_LOTSOFADDONS 0x20 // flag used to ask for full file list in d_netfil
#define MAXFILENEEDED 915 #define MAXFILENEEDED 915
@ -418,6 +421,22 @@ struct netinfo_pak
UINT32 delay[MAXPLAYERS+1]; UINT32 delay[MAXPLAYERS+1];
} ATTRPACK; } ATTRPACK;
// Sent by both sides. Contains Opus-encoded voice packet
// flags bitset map (left to right, low to high)
// | PPPPPTRR | -- P = Player num, T = Terminal, R = Reserved (0)
// Data following voice header is a single Opus frame
struct voice_pak
{
UINT64 frame;
UINT8 flags;
} ATTRPACK;
#define VOICE_PAK_FLAGS_PLAYERNUM_BITS 0x1F
#define VOICE_PAK_FLAGS_TERMINAL_BIT 0x20
#define VOICE_PAK_FLAGS_RESERVED0_BIT 0x40
#define VOICE_PAK_FLAGS_RESERVED1_BIT 0x80
#define VOICE_PAK_FLAGS_RESERVED_BITS (VOICE_PAK_FLAGS_RESERVED0_BIT | VOICE_PAK_FLAGS_RESERVED1_BIT)
// //
// Network packet data // Network packet data
// //
@ -462,6 +481,7 @@ struct doomdata_t
resultsall_pak resultsall; // 1024 bytes. Also, you really shouldn't trust anything here. resultsall_pak resultsall; // 1024 bytes. Also, you really shouldn't trust anything here.
say_pak say; // I don't care anymore. say_pak say; // I don't care anymore.
reqmapqueue_pak reqmapqueue; // Formerly XD_REQMAPQUEUE reqmapqueue_pak reqmapqueue; // Formerly XD_REQMAPQUEUE
voice_pak voice; // Unreliable voice data, variable length
} u; // This is needed to pack diff packet types data together } u; // This is needed to pack diff packet types data together
} ATTRPACK; } ATTRPACK;
@ -606,6 +626,7 @@ void SendKick(UINT8 playernum, UINT8 msg);
// Create any new ticcmds and broadcast to other players. // Create any new ticcmds and broadcast to other players.
void NetKeepAlive(void); void NetKeepAlive(void);
void NetUpdate(void); void NetUpdate(void);
void NetVoiceUpdate(void);
void SV_StartSinglePlayerServer(INT32 dogametype, boolean donetgame); void SV_StartSinglePlayerServer(INT32 dogametype, boolean donetgame);
boolean SV_SpawnServer(void); boolean SV_SpawnServer(void);
@ -710,6 +731,7 @@ void HandleSigfail(const char *string);
void DoSayPacket(SINT8 target, UINT8 flags, UINT8 source, char *message); void DoSayPacket(SINT8 target, UINT8 flags, UINT8 source, char *message);
void DoSayPacketFromCommand(SINT8 target, size_t usedargs, UINT8 flags); void DoSayPacketFromCommand(SINT8 target, size_t usedargs, UINT8 flags);
void DoVoicePacket(SINT8 target, UINT64 frame, const UINT8* opusdata, size_t len);
void SendServerNotice(SINT8 target, char *message); void SendServerNotice(SINT8 target, char *message);
#ifdef __cplusplus #ifdef __cplusplus

View file

@ -161,6 +161,7 @@ INT32 postimgparam[MAXSPLITSCREENPLAYERS];
boolean sound_disabled = false; boolean sound_disabled = false;
boolean digital_disabled = false; boolean digital_disabled = false;
boolean g_voice_disabled = false;
#ifdef DEBUGFILE #ifdef DEBUGFILE
INT32 debugload = 0; INT32 debugload = 0;
@ -1079,6 +1080,7 @@ void D_SRB2Loop(void)
// consoleplayer -> displayplayers (hear sounds from viewpoint) // consoleplayer -> displayplayers (hear sounds from viewpoint)
S_UpdateSounds(); // move positional sounds S_UpdateSounds(); // move positional sounds
NetVoiceUpdate(); // update voice recording whenever possible
if (realtics > 0 || singletics) if (realtics > 0 || singletics)
{ {
S_UpdateClosedCaptions(); S_UpdateClosedCaptions();
@ -1095,6 +1097,7 @@ void D_SRB2Loop(void)
#endif #endif
Music_Tick(); Music_Tick();
S_UpdateVoicePositionalProperties();
// Fully completed frame made. // Fully completed frame made.
finishprecise = I_GetPreciseTime(); finishprecise = I_GetPreciseTime();
@ -1885,12 +1888,14 @@ void D_SRB2Main(void)
{ {
sound_disabled = true; sound_disabled = true;
digital_disabled = true; digital_disabled = true;
g_voice_disabled = true;
} }
if (M_CheckParm("-noaudio")) // combines -nosound and -nomusic if (M_CheckParm("-noaudio")) // combines -nosound and -nomusic
{ {
sound_disabled = true; sound_disabled = true;
digital_disabled = true; digital_disabled = true;
g_voice_disabled = true;
} }
else else
{ {
@ -1905,9 +1910,13 @@ void D_SRB2Main(void)
if (M_CheckParm("-nodigmusic")) if (M_CheckParm("-nodigmusic"))
digital_disabled = true; // WARNING: DOS version initmusic in I_StartupSound digital_disabled = true; // WARNING: DOS version initmusic in I_StartupSound
} }
if (M_CheckParm("-novoice"))
{
g_voice_disabled = true;
}
} }
if (!( sound_disabled && digital_disabled )) if (!( sound_disabled && digital_disabled && g_voice_disabled ))
{ {
CONS_Printf("S_InitSfxChannels(): Setting up sound channels.\n"); CONS_Printf("S_InitSfxChannels(): Setting up sound channels.\n");
I_StartupSound(); I_StartupSound();

View file

@ -1175,6 +1175,8 @@ enum {
WP_AUTOROULETTE = 1<<2, WP_AUTOROULETTE = 1<<2,
WP_ANALOGSTICK = 1<<3, WP_ANALOGSTICK = 1<<3,
WP_AUTORING = 1<<4, WP_AUTORING = 1<<4,
WP_SELFMUTE = 1<<5,
WP_SELFDEAFEN = 1<<6
}; };
void WeaponPref_Send(UINT8 ssplayer) void WeaponPref_Send(UINT8 ssplayer)
@ -1196,6 +1198,15 @@ void WeaponPref_Send(UINT8 ssplayer)
if (cv_autoring[ssplayer].value) if (cv_autoring[ssplayer].value)
prefs |= WP_AUTORING; prefs |= WP_AUTORING;
if (ssplayer == 0)
{
if (cv_voice_selfmute.value)
prefs |= WP_SELFMUTE;
if (!cv_voice_chat.value)
prefs |= WP_SELFDEAFEN;
}
UINT8 buf[2]; UINT8 buf[2];
buf[0] = prefs; buf[0] = prefs;
buf[1] = cv_mindelay.value; buf[1] = cv_mindelay.value;
@ -1235,6 +1246,7 @@ size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum)
UINT8 prefs = READUINT8(p); UINT8 prefs = READUINT8(p);
player->pflags &= ~(PF_KICKSTARTACCEL|PF_SHRINKME|PF_AUTOROULETTE|PF_AUTORING); player->pflags &= ~(PF_KICKSTARTACCEL|PF_SHRINKME|PF_AUTOROULETTE|PF_AUTORING);
player->pflags2 &= ~(PF2_SELFMUTE | PF2_SELFDEAFEN);
if (prefs & WP_KICKSTARTACCEL) if (prefs & WP_KICKSTARTACCEL)
player->pflags |= PF_KICKSTARTACCEL; player->pflags |= PF_KICKSTARTACCEL;
@ -1253,6 +1265,12 @@ size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum)
if (prefs & WP_AUTORING) if (prefs & WP_AUTORING)
player->pflags |= PF_AUTORING; player->pflags |= PF_AUTORING;
if (prefs & WP_SELFMUTE)
player->pflags2 |= PF2_SELFMUTE;
if (prefs & WP_SELFDEAFEN)
player->pflags2 |= PF2_SELFDEAFEN;
if (leveltime < 2) if (leveltime < 2)
{ {
// BAD HACK: No other place I tried to slot this in // BAD HACK: No other place I tried to slot this in
@ -7032,6 +7050,18 @@ void Mute_OnChange(void)
HU_AddChatText(M_GetText("\x82*Chat is no longer muted."), false); HU_AddChatText(M_GetText("\x82*Chat is no longer muted."), false);
} }
void VoiceMute_OnChange(void);
void VoiceMute_OnChange(void)
{
if (leveltime <= 1)
return; // avoid having this notification put in our console / log when we boot the server.
if (cv_voice_servermute.value)
HU_AddChatText(M_GetText("\x82*Voice chat has been muted."), false);
else
HU_AddChatText(M_GetText("\x82*Voice chat is no longer muted."), false);
}
/** Hack to clear all changed flags after game start. /** Hack to clear all changed flags after game start.
* A lot of code (written by dummies, obviously) uses COM_BufAddText() to run * A lot of code (written by dummies, obviously) uses COM_BufAddText() to run
* commands and change consvars, especially on game start. This is problematic * commands and change consvars, especially on game start. This is problematic

View file

@ -61,6 +61,7 @@ extern consvar_t cv_netstat;
extern consvar_t cv_countdowntime; extern consvar_t cv_countdowntime;
extern consvar_t cv_mute; extern consvar_t cv_mute;
extern consvar_t cv_voice_servermute;
extern consvar_t cv_pause; extern consvar_t cv_pause;
extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_maxplayers; extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_maxplayers;
@ -185,6 +186,8 @@ typedef enum
XD_CALLZVOTE, // 39 XD_CALLZVOTE, // 39
XD_SETZVOTE, // 40 XD_SETZVOTE, // 40
XD_TEAMCHANGE, // 41 XD_TEAMCHANGE, // 41
XD_SERVERMUTEPLAYER, // 42
XD_SERVERDEAFENPLAYER, // 43
MAXNETXCMD MAXNETXCMD
} netxcmd_t; } netxcmd_t;

View file

@ -132,6 +132,14 @@ typedef enum
PF_NOFASTFALL = (INT32)(1U<<31), // Has already done ebrake/fastfall behavior for this input. Fastfalling needs a new input to prevent unwanted bounces on unexpected airtime. PF_NOFASTFALL = (INT32)(1U<<31), // Has already done ebrake/fastfall behavior for this input. Fastfalling needs a new input to prevent unwanted bounces on unexpected airtime.
} pflags_t; } pflags_t;
typedef enum
{
PF2_SELFMUTE = 1<<1,
PF2_SELFDEAFEN = 1<<2,
PF2_SERVERMUTE = 1<<3,
PF2_SERVERDEAFEN = 1<<4,
} pflags2_t;
typedef enum typedef enum
{ {
// Are animation frames playing? // Are animation frames playing?
@ -634,6 +642,7 @@ struct player_t
// Bit flags. // Bit flags.
// See pflags_t, above. // See pflags_t, above.
UINT32 pflags; UINT32 pflags;
UINT32 pflags2;
// playing animation. // playing animation.
panim_t panim; panim_t panim;

View file

@ -239,6 +239,7 @@ extern boolean forceresetplayers, deferencoremode, forcespecialstage;
extern boolean sound_disabled; extern boolean sound_disabled;
extern boolean digital_disabled; extern boolean digital_disabled;
extern boolean g_voice_disabled;
// ========================= // =========================
// Status flags for refresh. // Status flags for refresh.

View file

@ -224,3 +224,30 @@ boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
(void)looping; (void)looping;
return false; return false;
} }
boolean I_SoundInputIsEnabled(void)
{
return false;
}
boolean I_SoundInputSetEnabled(boolean enabled)
{
return false;
}
UINT32 I_SoundInputDequeueSamples(void *data, UINT32 len)
{
return 0;
}
void I_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal)
{
}
void I_SetPlayerVoiceProperties(INT32 playernum, float volume, float sep)
{
}
void I_ResetVoiceQueue(INT32 playernum)
{
}

View file

@ -2201,6 +2201,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
UINT32 followitem; UINT32 followitem;
INT32 pflags; INT32 pflags;
INT32 pflags2;
UINT8 team; UINT8 team;
@ -2348,6 +2349,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
xtralife = players[player].xtralife; xtralife = players[player].xtralife;
pflags = (players[player].pflags & (PF_WANTSTOJOIN|PF_KICKSTARTACCEL|PF_SHRINKME|PF_SHRINKACTIVE|PF_AUTOROULETTE|PF_ANALOGSTICK|PF_AUTORING)); pflags = (players[player].pflags & (PF_WANTSTOJOIN|PF_KICKSTARTACCEL|PF_SHRINKME|PF_SHRINKACTIVE|PF_AUTOROULETTE|PF_ANALOGSTICK|PF_AUTORING));
pflags2 = (players[player].pflags2 & (PF2_SELFMUTE | PF2_SELFDEAFEN | PF2_SERVERMUTE | PF2_SERVERDEAFEN));
// SRB2kart // SRB2kart
memcpy(&itemRoulette, &players[player].itemRoulette, sizeof (itemRoulette)); memcpy(&itemRoulette, &players[player].itemRoulette, sizeof (itemRoulette));
@ -2539,6 +2541,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
p->roundscore = roundscore; p->roundscore = roundscore;
p->lives = lives; p->lives = lives;
p->pflags = pflags; p->pflags = pflags;
p->pflags2 = pflags2;
p->team = team; p->team = team;
p->jointime = jointime; p->jointime = jointime;
p->splitscreenindex = splitscreenindex; p->splitscreenindex = splitscreenindex;

View file

@ -910,6 +910,7 @@ static const char *gamecontrolname[num_gamecontrols] =
"screenshot", "screenshot",
"startmovie", "startmovie",
"startlossless", "startlossless",
"voicepushtotalk"
}; };
#define NUMKEYNAMES (sizeof (keynames)/sizeof (keyname_t)) #define NUMKEYNAMES (sizeof (keynames)/sizeof (keyname_t))

View file

@ -114,6 +114,7 @@ typedef enum
gc_screenshot, gc_screenshot,
gc_startmovie, gc_startmovie,
gc_startlossless, gc_startlossless,
gc_voicepushtotalk,
num_gamecontrols, num_gamecontrols,

View file

@ -86,6 +86,7 @@ patch_t *frameslash; // framerate stuff. Used in screen.c
static player_t *plr; static player_t *plr;
boolean hu_keystrokes; // :) boolean hu_keystrokes; // :)
boolean chat_on; // entering a chat message? boolean chat_on; // entering a chat message?
boolean g_voicepushtotalk_on; // holding PTT?
static char w_chat[HU_MAXMSGLEN + 1]; static char w_chat[HU_MAXMSGLEN + 1];
static size_t c_input = 0; // let's try to make the chat input less shitty. static size_t c_input = 0; // let's try to make the chat input less shitty.
static boolean headsupactive = false; static boolean headsupactive = false;
@ -1102,6 +1103,24 @@ void HU_clearChatChars(void)
// //
boolean HU_Responder(event_t *ev) boolean HU_Responder(event_t *ev)
{ {
// Handle Push-to-Talk
if (ev->data1 == gamecontrol[0][gc_voicepushtotalk][0]
|| ev->data1 == gamecontrol[0][gc_voicepushtotalk][1]
|| ev->data1 == gamecontrol[0][gc_voicepushtotalk][2]
|| ev->data1 == gamecontrol[0][gc_voicepushtotalk][3])
{
if (ev->type == ev_keydown)
{
g_voicepushtotalk_on = true;
return true;
}
else if (ev->type == ev_keyup)
{
g_voicepushtotalk_on = false;
return true;
}
}
if (ev->type != ev_keydown) if (ev->type != ev_keydown)
return false; return false;

View file

@ -125,6 +125,9 @@ void HU_AddChatText(const char *text, boolean playsound);
// set true when entering a chat message // set true when entering a chat message
extern boolean chat_on; extern boolean chat_on;
// set true when push-to-talk is held
extern boolean g_voicepushtotalk_on;
// keystrokes in the console or chat window // keystrokes in the console or chat window
extern boolean hu_keystrokes; extern boolean hu_keystrokes;

View file

@ -27,6 +27,7 @@
#include "../k_hud.h" #include "../k_hud.h"
#include "../p_local.h" #include "../p_local.h"
#include "../r_fps.h" #include "../r_fps.h"
#include "../s_sound.h"
extern "C" consvar_t cv_maxplayers; extern "C" consvar_t cv_maxplayers;

View file

@ -114,6 +114,8 @@ void I_UpdateSoundParams(INT32 handle, UINT8 vol, UINT8 sep, UINT8 pitch);
*/ */
void I_SetSfxVolume(int volume); void I_SetSfxVolume(int volume);
void I_SetVoiceVolume(int volume);
/// ------------------------ /// ------------------------
// MUSIC SYSTEM // MUSIC SYSTEM
/// ------------------------ /// ------------------------
@ -246,6 +248,22 @@ boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
boolean I_FadeOutStopSong(UINT32 ms); boolean I_FadeOutStopSong(UINT32 ms);
boolean I_FadeInPlaySong(UINT32 ms, boolean looping); boolean I_FadeInPlaySong(UINT32 ms, boolean looping);
// AUDIO INPUT (Microphones)
boolean I_SoundInputIsEnabled(void);
boolean I_SoundInputSetEnabled(boolean enabled);
UINT32 I_SoundInputDequeueSamples(void *data, UINT32 len);
// VOICE CHAT
/// Queue a frame of samples of voice data from a player. Voice format is MONO F32 SYSTEM ENDIANNESS.
/// If there is too much data being queued, old samples will be truncated
void I_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal);
void I_SetPlayerVoiceProperties(INT32 playernum, float volume, float sep);
/// Reset the voice queue for the given player. Use when server connection ends
void I_ResetVoiceQueue(INT32 playernum);
#ifdef __cplusplus #ifdef __cplusplus
} // extern "C" } // extern "C"
#endif #endif

View file

@ -14,7 +14,7 @@
#include <vector> #include <vector>
#include <deque> #include <deque>
#include "v_draw.hpp" #include <fmt/format.h>
#include "k_hud.h" #include "k_hud.h"
#include "k_kart.h" #include "k_kart.h"
@ -54,6 +54,7 @@
#include "k_dialogue.h" #include "k_dialogue.h"
#include "f_finale.h" #include "f_finale.h"
#include "m_easing.h" #include "m_easing.h"
#include "v_draw.hpp"
//{ Patch Definitions //{ Patch Definitions
static patch_t *kp_nodraw; static patch_t *kp_nodraw;
@ -215,6 +216,17 @@ static patch_t *kp_bossret[4];
static patch_t *kp_trickcool[2]; static patch_t *kp_trickcool[2];
static patch_t *kp_voice_localactive[16];
static patch_t *kp_voice_localactiveoverlay[16];
static patch_t *kp_voice_localopen;
static patch_t *kp_voice_localmuted;
static patch_t *kp_voice_localdeafened;
static patch_t *kp_voice_remoteactive;
static patch_t *kp_voice_remoteopen;
static patch_t *kp_voice_remotemuted;
static patch_t *kp_voice_remotedeafened;
static patch_t *kp_voice_tagactive[3];
patch_t *kp_autoroulette; patch_t *kp_autoroulette;
patch_t *kp_autoring; patch_t *kp_autoring;
@ -1010,6 +1022,23 @@ void K_LoadKartHUDGraphics(void)
K_LoadGenericButtonGraphics(gen_button_rs, "R3"); K_LoadGenericButtonGraphics(gen_button_rs, "R3");
K_LoadGenericButtonGraphics(gen_button_start, "S"); K_LoadGenericButtonGraphics(gen_button_start, "S");
K_LoadGenericButtonGraphics(gen_button_back, "I"); K_LoadGenericButtonGraphics(gen_button_back, "I");
HU_UpdatePatch(&kp_voice_localopen, "VOXCLO");
for (i = 0; i < 16; i++)
{
HU_UpdatePatch(&kp_voice_localactive[i], "VOXCLA%d", i);
HU_UpdatePatch(&kp_voice_localactiveoverlay[i], "VOXCLB%d", i);
}
HU_UpdatePatch(&kp_voice_localmuted, "VOXCLM");
HU_UpdatePatch(&kp_voice_localdeafened, "VOXCLD");
HU_UpdatePatch(&kp_voice_remoteopen, "VOXCRO");
HU_UpdatePatch(&kp_voice_remoteactive, "VOXCRA");
HU_UpdatePatch(&kp_voice_remotemuted, "VOXCRM");
HU_UpdatePatch(&kp_voice_remotedeafened, "VOXCRD");
for (i = 0; i < 3; i++)
{
HU_UpdatePatch(&kp_voice_tagactive[i], "VOXCTA%d", i);
}
} }
// For the item toggle menu // For the item toggle menu
@ -2711,6 +2740,30 @@ void PositionFacesInfo::draw_1p()
); );
} }
// Voice speaking indicator
if (netgame && !players[rankplayer[i]].bot && cv_voice_servermute.value == 0)
{
patch_t *voxmic;
if (S_IsPlayerVoiceActive(rankplayer[i]))
{
voxmic = kp_voice_remoteactive;
}
else if (players[rankplayer[i]].pflags2 & (PF2_SELFDEAFEN | PF2_SERVERDEAFEN))
{
voxmic = kp_voice_remotedeafened;
}
else if (players[rankplayer[i]].pflags2 & (PF2_SELFMUTE | PF2_SERVERMUTE))
{
voxmic = kp_voice_remotemuted;
}
else
{
voxmic = kp_voice_remoteopen;
}
V_DrawScaledPatch(FACE_X + 10, Y - 4, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, voxmic);
}
Y -= 18; Y -= 18;
} }
} }
@ -4106,15 +4159,23 @@ static void K_DrawTypingDot(fixed_t x, fixed_t y, UINT8 duration, player_t *p, I
static void K_DrawTypingNotifier(fixed_t x, fixed_t y, player_t *p, INT32 flags) static void K_DrawTypingNotifier(fixed_t x, fixed_t y, player_t *p, INT32 flags)
{ {
if (p->cmd.flags & TICCMD_TYPING) int playernum = p - players;
if (p->cmd.flags & TICCMD_TYPING || S_IsPlayerVoiceActive(playernum))
{ {
V_DrawFixedPatch(x, y, FRACUNIT, V_SPLITSCREEN|flags, kp_talk, NULL); V_DrawFixedPatch(x, y, FRACUNIT, V_SPLITSCREEN|flags, kp_talk, NULL);
}
if (p->cmd.flags & TICCMD_TYPING)
{
/* spacing closer with the last two looks a better most of the time */ /* spacing closer with the last two looks a better most of the time */
K_DrawTypingDot(x + 3*FRACUNIT, y, 15, p, flags); K_DrawTypingDot(x + 3*FRACUNIT, y, 15, p, flags);
K_DrawTypingDot(x + 6*FRACUNIT - FRACUNIT/3, y, 31, p, flags); K_DrawTypingDot(x + 6*FRACUNIT - FRACUNIT/3, y, 31, p, flags);
K_DrawTypingDot(x + 9*FRACUNIT - FRACUNIT/3, y, 47, p, flags); K_DrawTypingDot(x + 9*FRACUNIT - FRACUNIT/3, y, 47, p, flags);
} }
else if (S_IsPlayerVoiceActive(playernum))
{
patch_t* voxmic = kp_voice_tagactive[(leveltime / 3) % 3];
V_DrawFixedPatch(x + 6*FRACUNIT, y - 12*FRACUNIT, FRACUNIT, V_SPLITSCREEN|flags, voxmic, NULL);
}
} }
// see also K_drawKartItem // see also K_drawKartItem
@ -6806,6 +6867,23 @@ void K_drawKartHUD(void)
} }
} }
// TODO better voice chat speaking indicator integration for spectators
{
char speakingstring[2048];
memset(speakingstring, 0, sizeof(speakingstring));
for (int i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && players[i].spectator && S_IsPlayerVoiceActive(i))
{
strcat(speakingstring, player_names[i]);
strcat(speakingstring, " ");
}
}
V_DrawThinString(0, 0, V_SNAPTOTOP|V_SNAPTOLEFT, speakingstring);
}
// Draw the countdowns after everything else. // Draw the countdowns after everything else.
if (stplyr->lives <= 0 && stplyr->playerstate == PST_DEAD) if (stplyr->lives <= 0 && stplyr->playerstate == PST_DEAD)
{ {
@ -6917,6 +6995,48 @@ void K_drawKartHUD(void)
} }
} }
if (netgame && cv_voice_servermute.value == 0)
{
if (players[consoleplayer].pflags2 & (PF2_SELFMUTE | PF2_SERVERMUTE | PF2_SELFDEAFEN | PF2_SERVERDEAFEN))
{
patch_t* micmuted = kp_voice_localmuted;
V_DrawFixedPatch(-1 * FRACUNIT, (BASEVIDHEIGHT - 21) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, micmuted, NULL);
}
else if (S_IsPlayerVoiceActive(consoleplayer))
{
patch_t* micactivebase = kp_voice_localactive[(leveltime / 2) % 16];
patch_t* micactivetop = kp_voice_localactiveoverlay[(leveltime / 2) % 16];
UINT8* micactivecolormap = NULL;
if (g_local_voice_last_peak < 0.7)
{
micactivecolormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_GREEN, GTC_CACHE);
}
else if (g_local_voice_last_peak < 0.95)
{
micactivecolormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_YELLOW, GTC_CACHE);
}
else
{
micactivecolormap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_RED, GTC_CACHE);
}
V_DrawFixedPatch(-15 * FRACUNIT, (BASEVIDHEIGHT - 34) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, micactivebase, micactivecolormap);
V_DrawFixedPatch(-15 * FRACUNIT, (BASEVIDHEIGHT - 34) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, micactivetop, micactivecolormap);
}
else
{
patch_t* micopen = kp_voice_localopen;
V_DrawFixedPatch(-1 * FRACUNIT, (BASEVIDHEIGHT - 21) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, micopen, NULL);
}
// Deafen indicator
if (players[consoleplayer].pflags2 & (PF2_SELFDEAFEN | PF2_SERVERDEAFEN))
{
patch_t* deafened = kp_voice_localdeafened;
V_DrawFixedPatch(16 * FRACUNIT, (BASEVIDHEIGHT - 15) << FRACBITS, FRACUNIT, V_SNAPTOBOTTOM|V_SNAPTOLEFT, deafened, NULL);
}
}
debug: debug:
K_DrawWaypointDebugger(); K_DrawWaypointDebugger();
K_DrawBotDebugger(); K_DrawBotDebugger();

View file

@ -346,6 +346,7 @@ typedef enum
mopt_profiles = 0, mopt_profiles = 0,
mopt_video, mopt_video,
mopt_sound, mopt_sound,
mopt_voice,
mopt_hud, mopt_hud,
mopt_gameplay, mopt_gameplay,
mopt_server, mopt_server,
@ -468,6 +469,9 @@ extern menu_t OPTIONS_VideoAdvancedDef;
extern menuitem_t OPTIONS_Sound[]; extern menuitem_t OPTIONS_Sound[];
extern menu_t OPTIONS_SoundDef; extern menu_t OPTIONS_SoundDef;
extern menuitem_t OPTIONS_Voice[];
extern menu_t OPTIONS_VoiceDef;
extern menuitem_t OPTIONS_HUD[]; extern menuitem_t OPTIONS_HUD[];
extern menu_t OPTIONS_HUDDef; extern menu_t OPTIONS_HUDDef;

View file

@ -4178,6 +4178,8 @@ void M_DrawMPServerBrowser(void)
servpats[i] = W_CachePatchName(va("M_SERV%c", i + '1'), PU_CACHE); servpats[i] = W_CachePatchName(va("M_SERV%c", i + '1'), PU_CACHE);
gearpats[i] = W_CachePatchName(va("M_SGEAR%c", i + '1'), PU_CACHE); gearpats[i] = W_CachePatchName(va("M_SGEAR%c", i + '1'), PU_CACHE);
} }
patch_t *voicepat;
voicepat = W_CachePatchName("VOCRMU", PU_CACHE);
fixed_t text1loop = SHORT(text1->height)*FRACUNIT; fixed_t text1loop = SHORT(text1->height)*FRACUNIT;
fixed_t text2loop = SHORT(text2->width)*FRACUNIT; fixed_t text2loop = SHORT(text2->width)*FRACUNIT;
@ -4279,6 +4281,12 @@ void M_DrawMPServerBrowser(void)
V_DrawFixedPatch((startx + 251)*FRACUNIT, (starty + ypos + 9)*FRACUNIT, FRACUNIT, transflag, gearpats[speed], NULL); V_DrawFixedPatch((startx + 251)*FRACUNIT, (starty + ypos + 9)*FRACUNIT, FRACUNIT, transflag, gearpats[speed], NULL);
} }
} }
// voice chat enabled
if (serverlist[i].info.kartvars & SV_VOICEENABLED)
{
V_DrawFixedPatch((startx - 3) * FRACUNIT, (starty + 2) * FRACUNIT, FRACUNIT, 0, voicepat, NULL);
}
} }
ypos += SERVERSPACE; ypos += SERVERSPACE;
} }

View file

@ -310,10 +310,12 @@ void PR_SaveProfiles(void)
for (size_t j = 0; j < num_gamecontrols; j++) for (size_t j = 0; j < num_gamecontrols; j++)
{ {
std::vector<int32_t> mappings;
for (size_t k = 0; k < MAXINPUTMAPPING; k++) for (size_t k = 0; k < MAXINPUTMAPPING; k++)
{ {
jsonprof.controls[j][k] = cprof->controls[j][k]; mappings.push_back(cprof->controls[j][k]);
} }
jsonprof.controls.emplace_back(std::move(mappings));
} }
ng.profiles.emplace_back(std::move(jsonprof)); ng.profiles.emplace_back(std::move(jsonprof));
@ -497,10 +499,25 @@ void PR_LoadProfiles(void)
try try
{ {
for (size_t j = 0; j < num_gamecontrols; j++) for (size_t j = 0; j < num_gamecontrols; j++)
{
if (jsprof.controls.size() <= j)
{ {
for (size_t k = 0; k < MAXINPUTMAPPING; k++) for (size_t k = 0; k < MAXINPUTMAPPING; k++)
{ {
newprof->controls[j][k] = jsprof.controls.at(j).at(k); newprof->controls[j][k] = gamecontroldefault[j][k];
}
continue;
}
auto& mappings = jsprof.controls.at(j);
for (size_t k = 0; k < MAXINPUTMAPPING; k++)
{
if (mappings.size() <= k)
{
newprof->controls[j][k] = 0;
continue;
}
newprof->controls[j][k] = mappings.at(k);
} }
} }
} }

View file

@ -77,7 +77,7 @@ struct ProfileJson
std::string followercolorname; std::string followercolorname;
ProfileRecordsJson records; ProfileRecordsJson records;
ProfilePreferencesJson preferences; ProfilePreferencesJson preferences;
std::array<std::array<int32_t, MAXINPUTMAPPING>, gamecontrols_e::num_gamecontrols> controls = {{{{}}}}; std::vector<std::vector<int32_t>> controls = {};
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT( NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
ProfileJson, ProfileJson,

View file

@ -1015,6 +1015,23 @@ void Y_VoteDrawer(void)
); );
} }
// TODO better voice chat speaking indicator integration
{
char speakingstring[2048];
memset(speakingstring, 0, sizeof(speakingstring));
for (int i = 0; i < MAXPLAYERS; i++)
{
if (S_IsPlayerVoiceActive(i))
{
strcat(speakingstring, player_names[i]);
strcat(speakingstring, " ");
}
}
V_DrawThinString(0, 0, 0, speakingstring);
}
M_DrawMenuForeground(); M_DrawMenuForeground();
} }

View file

@ -58,6 +58,25 @@ static void K_MidVoteKick(void)
SendKick(g_midVote.victim - players, KICK_MSG_VOTE_KICK); SendKick(g_midVote.victim - players, KICK_MSG_VOTE_KICK);
} }
/*--------------------------------------------------
static void K_MidVoteMute(void)
MVT_MUTE's success function.
--------------------------------------------------*/
static void K_MidVoteMute(void)
{
UINT8 buf[2];
if (g_midVote.victim == NULL)
{
return;
}
buf[0] = g_midVote.victim - players;
buf[1] = 1;
SendNetXCmd(XD_SERVERMUTEPLAYER, &buf, 2);
}
/*-------------------------------------------------- /*--------------------------------------------------
static void K_MidVoteRockTheVote(void) static void K_MidVoteRockTheVote(void)
@ -99,6 +118,13 @@ static midVoteTypeDef_t g_midVoteTypeDefs[MVT__MAX] =
K_MidVoteKick K_MidVoteKick
}, },
{ // MVT_MUTE
"MUTE",
"Mute Player?",
CVAR_INIT ("zvote_mute_allowed", "Yes", CV_SAVE|CV_NETVAR, CV_YesNo, NULL),
K_MidVoteMute
},
{ // MVT_RTV { // MVT_RTV
"RTV", "RTV",
"Skip Level?", "Skip Level?",
@ -127,6 +153,10 @@ boolean K_MidVoteTypeUsesVictim(midVoteType_e voteType)
{ {
return true; return true;
} }
case MVT_MUTE:
{
return true;
}
default: default:
{ {
return false; return false;
@ -1186,6 +1216,7 @@ void K_DrawMidVote(void)
switch (g_midVote.type) switch (g_midVote.type)
{ {
case MVT_KICK: case MVT_KICK:
case MVT_MUTE:
{ {
// Draw victim name // Draw victim name
if (g_midVote.victim != NULL) if (g_midVote.victim != NULL)

View file

@ -27,6 +27,7 @@ extern "C" {
typedef enum typedef enum
{ {
MVT_KICK, // Kick another player in the server MVT_KICK, // Kick another player in the server
MVT_MUTE, // Mute another player in the server (Voice Chat)
MVT_RTV, // Exit level early MVT_RTV, // Exit level early
MVT_RUNITBACK, // Restart level fresh MVT_RUNITBACK, // Restart level fresh
MVT__MAX, // Total number of vote types MVT__MAX, // Total number of vote types

View file

@ -35,18 +35,39 @@ extern "C" {
| \ | \
(((UINT32)(x) & (UINT32)0xff000000UL) >> 24))) (((UINT32)(x) & (UINT32)0xff000000UL) >> 24)))
#define SWAP_LONGLONG(x) ((INT64)(\
(((UINT64)(x) & (UINT64)0x00000000000000ffULL) << 56) \
| \
(((UINT64)(x) & (UINT64)0x000000000000ff00ULL) << 40) \
| \
(((UINT64)(x) & (UINT64)0x0000000000ff0000ULL) << 24) \
| \
(((UINT64)(x) & (UINT64)0x00000000ff000000ULL) << 8) \
| \
(((UINT64)(x) & (UINT64)0x000000ff00000000ULL) >> 8) \
| \
(((UINT64)(x) & (UINT64)0x0000ff0000000000ULL) >> 24) \
| \
(((UINT64)(x) & (UINT64)0x00ff000000000000ULL) >> 40) \
| \
(((UINT64)(x) & (UINT64)0xff00000000000000ULL) >> 56)))
// Endianess handling. // Endianess handling.
// WAD files are stored little endian. // WAD files are stored little endian.
#ifdef SRB2_BIG_ENDIAN #ifdef SRB2_BIG_ENDIAN
#define SHORT SWAP_SHORT #define SHORT SWAP_SHORT
#define LONG SWAP_LONG #define LONG SWAP_LONG
#define LONGLON SWAP_LONGLONG
#define MSBF_SHORT(x) ((INT16)(x)) #define MSBF_SHORT(x) ((INT16)(x))
#define MSBF_LONG(x) ((INT32)(x)) #define MSBF_LONG(x) ((INT32)(x))
#define MSBF_LONGLONG(x) ((INT64)(x))
#else #else
#define SHORT(x) ((INT16)(x)) #define SHORT(x) ((INT16)(x))
#define LONG(x) ((INT32)(x)) #define LONG(x) ((INT32)(x))
#define LONGLONG(x) ((INT64)(x))
#define MSBF_SHORT SWAP_SHORT #define MSBF_SHORT SWAP_SHORT
#define MSBF_LONG SWAP_LONG #define MSBF_LONG SWAP_LONG
#define MSBF_LONGLONG SWAP_LONGLONG
#endif #endif
// Big to little endian // Big to little endian

View file

@ -29,6 +29,7 @@ target_sources(SRB2SDL2 PRIVATE
options-video-1.c options-video-1.c
options-video-advanced.c options-video-advanced.c
options-video-modes.c options-video-modes.c
options-voice.cpp
play-1.c play-1.c
play-char-select.c play-char-select.c
play-local-1.c play-local-1.c

View file

@ -31,6 +31,9 @@ menuitem_t OPTIONS_Main[] =
{IT_STRING | IT_CALL, "Sound Options", "Adjust the volume.", {IT_STRING | IT_CALL, "Sound Options", "Adjust the volume.",
NULL, {.routine = M_SoundOptions}, 0, 0}, NULL, {.routine = M_SoundOptions}, 0, 0},
{IT_STRING | IT_SUBMENU, "Voice Options", "Adjust voice chat.",
NULL, {.submenu = &OPTIONS_VoiceDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "HUD Options", "Tweak the Heads-Up Display.", {IT_STRING | IT_SUBMENU, "HUD Options", "Tweak the Heads-Up Display.",
NULL, {.submenu = &OPTIONS_HUDDef}, 0, 0}, NULL, {.submenu = &OPTIONS_HUDDef}, 0, 0},

View file

@ -87,6 +87,9 @@ menuitem_t OPTIONS_ProfileControls[] = {
{IT_CONTROL, "OPEN TEAM CHAT", "Opens team-only full chat for online games.", {IT_CONTROL, "OPEN TEAM CHAT", "Opens team-only full chat for online games.",
NULL, {.routine = M_ProfileSetControl}, gc_teamtalk, 0}, NULL, {.routine = M_ProfileSetControl}, gc_teamtalk, 0},
{IT_CONTROL, "PUSH-TO-TALK", "Activates voice chat transmission in Push-to-Talk (PTT) mode.",
NULL, {.routine = M_ProfileSetControl}, gc_voicepushtotalk, 0},
{IT_CONTROL, "LUA/1", "May be used by add-ons.", {IT_CONTROL, "LUA/1", "May be used by add-ons.",
NULL, {.routine = M_ProfileSetControl}, gc_lua1, 0}, NULL, {.routine = M_ProfileSetControl}, gc_lua1, 0},

View file

@ -78,6 +78,9 @@ menuitem_t OPTIONS_Server[] =
{IT_STRING | IT_CVAR, "Mute Chat", "Prevent everyone but admins from sending chat messages.", {IT_STRING | IT_CVAR, "Mute Chat", "Prevent everyone but admins from sending chat messages.",
NULL, {.cvar = &cv_mute}, 0, 0}, NULL, {.cvar = &cv_mute}, 0, 0},
{IT_STRING | IT_CVAR, "Mute Voice Chat", "Prevent everyone from sending voice chat.",
NULL, {.cvar = &cv_voice_servermute}, 0, 0},
{IT_STRING | IT_CVAR, "Chat Spam Protection", "Prevent too many messages from a single player.", {IT_STRING | IT_CVAR, "Chat Spam Protection", "Prevent too many messages from a single player.",
NULL, {.cvar = &cv_chatspamprotection}, 0, 0}, NULL, {.cvar = &cv_chatspamprotection}, 0, 0},

View file

@ -38,6 +38,7 @@ struct Slider
kMasterVolume, kMasterVolume,
kMusicVolume, kMusicVolume,
kSfxVolume, kSfxVolume,
kVoiceVolume,
kNumSliders kNumSliders
}; };
@ -120,6 +121,7 @@ std::array<Slider, Slider::kNumSliders> sliders{{
n = !n; n = !n;
CV_SetValue(&cv_gamedigimusic, n); CV_SetValue(&cv_gamedigimusic, n);
CV_SetValue(&cv_gamesounds, n); CV_SetValue(&cv_gamesounds, n);
CV_SetValue(&cv_voice_chat, n);
} }
return n; return n;
@ -150,6 +152,18 @@ std::array<Slider, Slider::kNumSliders> sliders{{
}, },
cv_soundvolume, cv_soundvolume,
}, },
{
[](bool toggle) -> bool
{
if (toggle)
{
CV_AddValue(&cv_voice_chat, 1);
}
return !S_VoiceDisabled();
},
cv_voicevolume,
},
}}; }};
void slider_routine(INT32 c) void slider_routine(INT32 c)
@ -266,6 +280,9 @@ menuitem_t OPTIONS_Sound[] =
{IT_STRING | IT_ARROWS | IT_CV_SLIDER, "Music Volume", "Loudness of music.", {IT_STRING | IT_ARROWS | IT_CV_SLIDER, "Music Volume", "Loudness of music.",
NULL, {.routine = slider_routine}, 0, Slider::kMusicVolume}, NULL, {.routine = slider_routine}, 0, Slider::kMusicVolume},
{IT_STRING | IT_ARROWS | IT_CV_SLIDER, "Voice Volume", "Loudness of voice chat.",
NULL, {.routine = slider_routine}, 0, Slider::kVoiceVolume},
{IT_SPACE | IT_NOTHING, NULL, NULL, {IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0}, NULL, {NULL}, 0, 0},

View file

@ -0,0 +1,91 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2024 by Kart Krew.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file menus/options-voice.cpp
/// \brief Voice Options
#include "../m_easing.h"
#include "../k_menu.h"
#include "../s_sound.h" // sounds consvars
#include "../v_video.h"
menuitem_t OPTIONS_Voice[] =
{
{IT_STRING | IT_CVAR, "Voice Chat", "Turn on or off all voice chat for yourself.",
NULL, {.cvar = &cv_voice_chat}, 0, 0},
{IT_STRING | IT_CVAR, "Voice Mode", "When to transmit your own voice.",
NULL, {.cvar = &cv_voice_mode}, 0, 0},
{IT_STRING | IT_CVAR, "Input Amplifier", "Amplify your voice, in decibels. Negative values are quieter.",
NULL, {.cvar = &cv_voice_inputamp}, 0, 0},
{IT_STRING | IT_CVAR, "Activation Threshold", "Voice higher than this threshold will transmit, in decibels.",
NULL, {.cvar = &cv_voice_activationthreshold}, 0, 0},
{IT_STRING | IT_CVAR, "Self Voice Mute", "Whether your voice is transmitted or not.",
NULL, {.cvar = &cv_voice_selfmute}, 0, 0},
{IT_STRING | IT_CVAR, "Voice Loopback", "Play your own recording voice back.",
NULL, {.cvar = &cv_voice_loopback}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_HEADER, "Server Voice Options...", NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Server Voice Mute", "All voice chat will be disabled on your server.",
NULL, {.cvar = &cv_voice_servermute}, 0, 0},
{IT_STRING | IT_CVAR, "Proximity Effects", "Player voices will be adjusted relative to you.",
NULL, {.cvar = &cv_voice_proximity}, 0, 0},
};
static void draw_routine()
{
M_DrawGenericOptions();
int x = currentMenu->x - M_EaseWithTransition(Easing_Linear, 5 * 48);
int y = currentMenu->y - 12;
int range = 220;
float last_peak = g_local_voice_last_peak * range;
boolean detected = g_local_voice_detected;
INT32 color = detected ? 65 : 23;
V_DrawFill(x, y, range + 2, 10, 31);
V_DrawFill(x + 1, y + 1, (int) last_peak, 8, color);
}
static void tick_routine()
{
M_OptionsTick();
}
static boolean input_routine(INT32)
{
return false;
}
menu_t OPTIONS_VoiceDef = {
sizeof (OPTIONS_Voice) / sizeof (menuitem_t),
&OPTIONS_MainDef,
0,
OPTIONS_Voice,
48, 80,
SKINCOLOR_ULTRAMARINE, 0,
MBF_DRAWBGWHILEPLAYING,
NULL,
2, 5,
draw_routine,
M_DrawOptionsCogs,
tick_routine,
NULL,
NULL,
input_routine,
};

View file

@ -266,6 +266,14 @@ boolean S_SoundDisabled(void)
); );
} }
boolean S_VoiceDisabled(void)
{
return (
g_voice_disabled ||
(g_fast_forward > 0)
);
}
// Stop all sounds, load level info, THEN start sounds. // Stop all sounds, load level info, THEN start sounds.
void S_StopSounds(void) void S_StopSounds(void)
{ {
@ -676,6 +684,13 @@ void S_StopSound(void *origin)
static INT32 actualsfxvolume; // check for change through console static INT32 actualsfxvolume; // check for change through console
static INT32 actualdigmusicvolume; static INT32 actualdigmusicvolume;
static INT32 actualmastervolume; static INT32 actualmastervolume;
static INT32 actualvoicevolume;
static boolean PointIsLeft(float ax, float ay, float bx, float by)
{
// return (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x) > 0;
return ax * by - ay * bx > 0;
}
void S_UpdateSounds(void) void S_UpdateSounds(void)
{ {
@ -694,6 +709,8 @@ void S_UpdateSounds(void)
S_SetMusicVolume(); S_SetMusicVolume();
if (actualmastervolume != cv_mastervolume.value) if (actualmastervolume != cv_mastervolume.value)
S_SetMasterVolume(); S_SetMasterVolume();
if (actualvoicevolume != cv_voicevolume.value)
S_SetVoiceVolume();
// We're done now, if we're not in a level. // We're done now, if we're not in a level.
if (gamestate != GS_LEVEL) if (gamestate != GS_LEVEL)
@ -853,6 +870,154 @@ notinlevel:
I_UpdateSound(); I_UpdateSound();
} }
void S_UpdateVoicePositionalProperties(void)
{
int i;
if (gamestate != GS_LEVEL)
{
for (i = 0; i < MAXPLAYERS; i++)
{
I_SetPlayerVoiceProperties(i, 1.0f, 0.0f);
}
return;
}
player_t *consoleplr = &players[consoleplayer];
listener_t listener = {0};
mobj_t *listenmobj = NULL;
if (consoleplr)
{
if (consoleplr->awayview.tics)
{
listenmobj = consoleplr->awayview.mobj;
}
else
{
listenmobj = consoleplr->mo;
}
if (camera[0].chase && !consoleplr->awayview.tics)
{
listener.x = camera[0].x;
listener.y = camera[0].y;
listener.z = camera[0].z;
listener.angle = camera[0].angle;
}
else if (listenmobj)
{
listener.x = listenmobj->x;
listener.y = listenmobj->y;
listener.z = listenmobj->z;
listener.angle = listenmobj->angle;
}
}
float playerdistances[MAXPLAYERS];
for (i = 0; i < MAXPLAYERS; i++)
{
player_t *plr = &players[i];
mobj_t *mo = plr->mo;
if (plr->spectator || !mo)
{
playerdistances[i] = 0.f;
continue;
}
float px = FixedToFloat(mo->x - listener.x);
float py = FixedToFloat(mo->y - listener.y);
float pz = FixedToFloat(mo->z - listener.z);
playerdistances[i] = sqrtf(px * px + py * py + pz * pz);
}
// Positional voice audio
boolean voice_proximity_enabled = cv_voice_proximity.value == 1;
float voice_distanceattenuation_distance = FixedToFloat(cv_voice_distanceattenuation_distance.value) * FixedToFloat(mapheaderinfo[gamemap-1]->mobj_scale);
float voice_distanceattenuation_factor = FixedToFloat(cv_voice_distanceattenuation_factor.value);
float voice_stereopanning_factor = FixedToFloat(cv_voice_stereopanning_factor.value);
float voice_concurrentattenuation_min = max(0, min(MAXPLAYERS, cv_voice_concurrentattenuation_min.value));
float voice_concurrentattenuation_max = max(0, min(MAXPLAYERS, cv_voice_concurrentattenuation_max.value));
voice_concurrentattenuation_min = min(voice_concurrentattenuation_max, voice_concurrentattenuation_min);
float voice_concurrentattenuation_factor = FixedToFloat(cv_voice_concurrentattenuation_factor.value);
// Derive concurrent speaker attenuation
float speakingplayers = 0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (S_IsPlayerVoiceActive(i))
{
if (voice_distanceattenuation_distance > 0)
{
speakingplayers += 1.f - (playerdistances[i] / voice_distanceattenuation_distance);
}
else
{
// invalid distance attenuation
speakingplayers += 1.f;
}
}
}
speakingplayers = min(voice_concurrentattenuation_max, max(0, speakingplayers - voice_concurrentattenuation_min));
float speakingplayerattenuation = 1.f - (speakingplayers * (voice_concurrentattenuation_factor / voice_concurrentattenuation_max));
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || !voice_proximity_enabled)
{
I_SetPlayerVoiceProperties(i, speakingplayerattenuation, 0.0f);
continue;
}
if (i == consoleplayer)
{
I_SetPlayerVoiceProperties(i, speakingplayerattenuation, 0.0f);
continue;
}
player_t *plr = &players[i];
if (plr->spectator)
{
I_SetPlayerVoiceProperties(i, speakingplayerattenuation, 0.0f);
continue;
}
mobj_t *plrmobj = plr->mo;
if (!plrmobj)
{
I_SetPlayerVoiceProperties(i, 1.0f, 0.0f);
continue;
}
float lx = FixedToFloat(listener.x);
float ly = FixedToFloat(listener.y);
float lz = FixedToFloat(listener.z);
float px = FixedToFloat(plrmobj->x) - lx;
float py = FixedToFloat(plrmobj->y) - ly;
float pz = FixedToFloat(plrmobj->z) - lz;
float ldirx = cosf(ANG2RAD(listener.angle));
float ldiry = sinf(ANG2RAD(listener.angle));
float pdistance = sqrtf(px * px + py * py + pz * pz);
float p2ddistance = sqrtf(px * px + py * py);
float pdirx = px / p2ddistance;
float pdiry = py / p2ddistance;
float angle = acosf(pdirx * ldirx + pdiry * ldiry);
angle = PointIsLeft(ldirx, ldiry, pdirx, pdiry) ? -angle : angle;
float plrvolume = 1.0f;
if (voice_distanceattenuation_distance > 0 && voice_distanceattenuation_factor >= 0 && voice_distanceattenuation_factor <= 1.0f)
{
float invfactor = 1.0f - voice_distanceattenuation_factor;
float distfactor = max(0.f, min(voice_distanceattenuation_distance, pdistance)) / voice_distanceattenuation_distance;
plrvolume = max(0.0f, min(1.0f, 1.0f - (invfactor * distfactor)));
}
float fsep = sinf(angle) * max(0.0f, min(1.0f, voice_stereopanning_factor));
I_SetPlayerVoiceProperties(i, plrvolume * speakingplayerattenuation, fsep);
}
}
void S_UpdateClosedCaptions(void) void S_UpdateClosedCaptions(void)
{ {
UINT8 i; UINT8 i;
@ -887,6 +1052,13 @@ void S_SetSfxVolume(void)
I_SetSfxVolume(actualsfxvolume); I_SetSfxVolume(actualsfxvolume);
} }
void S_SetVoiceVolume(void)
{
actualvoicevolume = cv_voicevolume.value;
I_SetVoiceVolume(actualvoicevolume);
}
void S_SetMasterVolume(void) void S_SetMasterVolume(void)
{ {
actualmastervolume = cv_mastervolume.value; actualmastervolume = cv_mastervolume.value;
@ -2639,6 +2811,18 @@ void GameDigiMusic_OnChange(void)
} }
} }
void VoiceChat_OnChange(void);
void weaponPrefChange(INT32 ssplayer);
void VoiceChat_OnChange(void)
{
if (M_CheckParm("-novoice") || M_CheckParm("-noaudio"))
return;
g_voice_disabled = !cv_voice_chat.value;
weaponPrefChange(0);
}
void BGAudio_OnChange(void); void BGAudio_OnChange(void);
void BGAudio_OnChange(void) void BGAudio_OnChange(void)
{ {
@ -2656,3 +2840,53 @@ void BGAudio_OnChange(void)
if (window_notinfocus && !(cv_bgaudio.value & 2)) if (window_notinfocus && !(cv_bgaudio.value & 2))
S_StopSounds(); S_StopSounds();
} }
boolean S_SoundInputIsEnabled(void)
{
return I_SoundInputIsEnabled();
}
boolean S_SoundInputSetEnabled(boolean enabled)
{
return I_SoundInputSetEnabled(enabled);
}
UINT32 S_SoundInputDequeueSamples(void *data, UINT32 len)
{
return I_SoundInputDequeueSamples(data, len);
}
static INT32 g_playerlastvoiceactive[MAXPLAYERS];
void S_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal)
{
if (dedicated)
{
return;
}
if (cv_voice_chat.value != 0)
{
I_QueueVoiceFrameFromPlayer(playernum, data, len, terminal);
}
}
void S_SetPlayerVoiceActive(INT32 playernum)
{
g_playerlastvoiceactive[playernum] = I_GetTime();
}
boolean S_IsPlayerVoiceActive(INT32 playernum)
{
return I_GetTime() - g_playerlastvoiceactive[playernum] < 5;
}
void S_ResetVoiceQueue(INT32 playernum)
{
if (dedicated)
{
return;
}
I_ResetVoiceQueue(playernum);
g_playerlastvoiceactive[playernum] = 0;
}

View file

@ -35,6 +35,7 @@ extern "C" {
extern consvar_t stereoreverse; extern consvar_t stereoreverse;
extern consvar_t cv_soundvolume, cv_closedcaptioning, cv_digmusicvolume; extern consvar_t cv_soundvolume, cv_closedcaptioning, cv_digmusicvolume;
extern consvar_t cv_voicevolume;
extern consvar_t surround; extern consvar_t surround;
extern consvar_t cv_numChannels; extern consvar_t cv_numChannels;
@ -47,6 +48,22 @@ extern consvar_t cv_gamesounds;
extern consvar_t cv_bgaudio; extern consvar_t cv_bgaudio;
extern consvar_t cv_streamersafemusic; extern consvar_t cv_streamersafemusic;
extern consvar_t cv_voice_chat;
extern consvar_t cv_voice_mode;
extern consvar_t cv_voice_selfmute;
extern consvar_t cv_voice_loopback;
extern consvar_t cv_voice_inputamp;
extern consvar_t cv_voice_activationthreshold;
extern consvar_t cv_voice_proximity;
extern consvar_t cv_voice_distanceattenuation_distance;
extern consvar_t cv_voice_distanceattenuation_factor;
extern consvar_t cv_voice_stereopanning_factor;
extern consvar_t cv_voice_concurrentattenuation_factor;
extern consvar_t cv_voice_concurrentattenuation_min;
extern consvar_t cv_voice_concurrentattenuation_max;
extern float g_local_voice_last_peak;
extern boolean g_local_voice_detected;
typedef enum typedef enum
{ {
SF_TOTALLYSINGLE = 1, // Only play one of these sounds at a time...GLOBALLY SF_TOTALLYSINGLE = 1, // Only play one of these sounds at a time...GLOBALLY
@ -122,6 +139,8 @@ lumpnum_t S_GetSfxLumpNum(sfxinfo_t *sfx);
boolean S_SoundDisabled(void); boolean S_SoundDisabled(void);
boolean S_VoiceDisabled(void);
// //
// Start sound for thing at <origin> using <sound_id> from sounds.h // Start sound for thing at <origin> using <sound_id> from sounds.h
// //
@ -249,6 +268,7 @@ void S_AttemptToRestoreMusic(void);
// //
void S_UpdateSounds(void); void S_UpdateSounds(void);
void S_UpdateClosedCaptions(void); void S_UpdateClosedCaptions(void);
void S_UpdateVoicePositionalProperties(void);
FUNCMATH fixed_t S_CalculateSoundDistance(fixed_t px1, fixed_t py1, fixed_t pz1, fixed_t px2, fixed_t py2, fixed_t pz2); FUNCMATH fixed_t S_CalculateSoundDistance(fixed_t px1, fixed_t py1, fixed_t pz1, fixed_t px2, fixed_t py2, fixed_t pz2);
@ -257,6 +277,7 @@ INT32 S_GetSoundVolume(sfxinfo_t *sfx, INT32 volume);
void S_SetSfxVolume(void); void S_SetSfxVolume(void);
void S_SetMusicVolume(void); void S_SetMusicVolume(void);
void S_SetMasterVolume(void); void S_SetMasterVolume(void);
void S_SetVoiceVolume(void);
INT32 S_OriginPlaying(void *origin); INT32 S_OriginPlaying(void *origin);
INT32 S_IdPlaying(sfxenum_t id); INT32 S_IdPlaying(sfxenum_t id);
@ -270,6 +291,15 @@ void S_StopSoundByNum(sfxenum_t sfxnum);
#define S_StartAttackSound S_StartSound #define S_StartAttackSound S_StartSound
#define S_StartScreamSound S_StartSound #define S_StartScreamSound S_StartSound
boolean S_SoundInputIsEnabled(void);
boolean S_SoundInputSetEnabled(boolean enabled);
UINT32 S_SoundInputDequeueSamples(void *data, UINT32 len);
void S_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal);
void S_SetPlayerVoiceActive(INT32 playernum);
boolean S_IsPlayerVoiceActive(INT32 playernum);
void S_ResetVoiceQueue(INT32 playernum);
#ifdef __cplusplus #ifdef __cplusplus
} // extern "C" } // extern "C"
#endif #endif

View file

@ -53,6 +53,125 @@ using srb2::audio::Source;
using namespace srb2; using namespace srb2;
using namespace srb2::io; using namespace srb2::io;
namespace
{
class SdlAudioStream final
{
SDL_AudioStream* stream_;
public:
SdlAudioStream(const SDL_AudioFormat format, const Uint8 channels, const int src_rate, const SDL_AudioFormat dst_format, const Uint8 dst_channels, const int dst_rate) noexcept
{
stream_ = SDL_NewAudioStream(format, channels, src_rate, dst_format, dst_channels, dst_rate);
}
SdlAudioStream(const SdlAudioStream&) = delete;
SdlAudioStream(SdlAudioStream&&) = default;
SdlAudioStream& operator=(const SdlAudioStream&) = delete;
SdlAudioStream& operator=(SdlAudioStream&&) = default;
~SdlAudioStream()
{
SDL_FreeAudioStream(stream_);
}
void put(tcb::span<const std::byte> buf)
{
int result = SDL_AudioStreamPut(stream_, buf.data(), buf.size_bytes());
if (result < 0)
{
char errbuf[512];
SDL_GetErrorMsg(errbuf, sizeof(errbuf));
throw std::runtime_error(errbuf);
}
}
size_t available() const
{
int result = SDL_AudioStreamAvailable(stream_);
if (result < 0)
{
char errbuf[512];
SDL_GetErrorMsg(errbuf, sizeof(errbuf));
throw std::runtime_error(errbuf);
}
return result;
}
size_t get(tcb::span<std::byte> out)
{
int result = SDL_AudioStreamGet(stream_, out.data(), out.size_bytes());
if (result < 0)
{
char errbuf[512];
SDL_GetErrorMsg(errbuf, sizeof(errbuf));
throw std::runtime_error(errbuf);
}
return result;
}
void clear() noexcept
{
SDL_AudioStreamClear(stream_);
}
};
class SdlVoiceStreamPlayer : public Source<2>
{
SdlAudioStream stream_;
float volume_ = 1.0f;
float sep_ = 0.0f;
bool terminal_ = true;
public:
SdlVoiceStreamPlayer() : stream_(AUDIO_F32SYS, 1, 48000, AUDIO_F32SYS, 2, 44100) {}
virtual ~SdlVoiceStreamPlayer() = default;
virtual std::size_t generate(tcb::span<Sample<2>> buffer) override
{
size_t written = stream_.get(tcb::as_writable_bytes(buffer)) / sizeof(Sample<2>);
for (size_t i = written; i < buffer.size(); i++)
{
buffer[i] = {0.f, 0.f};
}
// Apply gain de-popping if the last generation was terminal
if (terminal_)
{
for (size_t i = 0; i < std::min<size_t>(16, written); i++)
{
buffer[i].amplitudes[0] *= (float)(i) / 16;
buffer[i].amplitudes[1] *= (float)(i) / 16;
}
terminal_ = false;
}
if (written < buffer.size())
{
terminal_ = true;
}
for (size_t i = 0; i < written; i++)
{
float sep_pan = ((sep_ + 1.f) / 2.f) * (3.14159 / 2.f);
float left_scale = std::cos(sep_pan);
float right_scale = std::sin(sep_pan);
buffer[i] = {buffer[i].amplitudes[0] * volume_ * left_scale, buffer[i].amplitudes[1] * volume_ * right_scale};
}
return buffer.size();
};
SdlAudioStream& stream() noexcept { return stream_; }
void set_properties(float volume, float sep) noexcept
{
volume_ = volume;
sep_ = sep;
}
};
} // namespace
// extern in i_sound.h // extern in i_sound.h
UINT8 sound_started = false; UINT8 sound_started = false;
@ -60,13 +179,16 @@ static unique_ptr<Gain<2>> master_gain;
static shared_ptr<Mixer<2>> master; static shared_ptr<Mixer<2>> master;
static shared_ptr<Mixer<2>> mixer_sound_effects; static shared_ptr<Mixer<2>> mixer_sound_effects;
static shared_ptr<Mixer<2>> mixer_music; static shared_ptr<Mixer<2>> mixer_music;
static shared_ptr<Mixer<2>> mixer_voice;
static shared_ptr<MusicPlayer> music_player; static shared_ptr<MusicPlayer> music_player;
static shared_ptr<Resampler<2>> resample_music_player; static shared_ptr<Resampler<2>> resample_music_player;
static shared_ptr<Gain<2>> gain_sound_effects; static shared_ptr<Gain<2>> gain_sound_effects;
static shared_ptr<Gain<2>> gain_music_player; static shared_ptr<Gain<2>> gain_music_player;
static shared_ptr<Gain<2>> gain_music_channel; static shared_ptr<Gain<2>> gain_music_channel;
static shared_ptr<Gain<2>> gain_voice_channel;
static vector<shared_ptr<SoundEffectPlayer>> sound_effect_channels; static vector<shared_ptr<SoundEffectPlayer>> sound_effect_channels;
static vector<shared_ptr<SdlVoiceStreamPlayer>> player_voice_channels;
#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES #ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES
static shared_ptr<srb2::media::AVRecorder> av_recorder; static shared_ptr<srb2::media::AVRecorder> av_recorder;
@ -74,6 +196,10 @@ static shared_ptr<srb2::media::AVRecorder> av_recorder;
static void (*music_fade_callback)(); static void (*music_fade_callback)();
static SDL_AudioDeviceID g_device_id;
static SDL_AudioDeviceID g_input_device_id;
static boolean g_input_device_paused;
void* I_GetSfx(sfxinfo_t* sfx) void* I_GetSfx(sfxinfo_t* sfx)
{ {
if (sfx->lumpnum == LUMPERROR) if (sfx->lumpnum == LUMPERROR)
@ -120,8 +246,8 @@ namespace
class SdlAudioLockHandle class SdlAudioLockHandle
{ {
public: public:
SdlAudioLockHandle() { SDL_LockAudio(); } SdlAudioLockHandle() { SDL_LockAudioDevice(g_device_id); }
~SdlAudioLockHandle() { SDL_UnlockAudio(); } ~SdlAudioLockHandle() { SDL_UnlockAudioDevice(g_device_id); }
}; };
#ifdef TRACY_ENABLE #ifdef TRACY_ENABLE
@ -180,21 +306,21 @@ void initialize_sound()
return; return;
} }
SDL_AudioSpec desired; SDL_AudioSpec desired {};
desired.format = AUDIO_F32SYS; desired.format = AUDIO_F32SYS;
desired.channels = 2; desired.channels = 2;
desired.samples = cv_soundmixingbuffersize.value; desired.samples = cv_soundmixingbuffersize.value;
desired.freq = 44100; desired.freq = 44100;
desired.callback = audio_callback; desired.callback = audio_callback;
if (SDL_OpenAudio(&desired, NULL) < 0) if ((g_device_id = SDL_OpenAudioDevice(NULL, SDL_FALSE, &desired, NULL, 0)) == 0)
{ {
CONS_Alert(CONS_ERROR, "Failed to open SDL Audio device: %s\n", SDL_GetError()); CONS_Alert(CONS_ERROR, "Failed to open SDL Audio device: %s\n", SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO); SDL_QuitSubSystem(SDL_INIT_AUDIO);
return; return;
} }
SDL_PauseAudio(SDL_FALSE); SDL_PauseAudioDevice(g_device_id, SDL_FALSE);
{ {
SdlAudioLockHandle _; SdlAudioLockHandle _;
@ -204,16 +330,20 @@ void initialize_sound()
master_gain->bind(master); master_gain->bind(master);
mixer_sound_effects = make_shared<Mixer<2>>(); mixer_sound_effects = make_shared<Mixer<2>>();
mixer_music = make_shared<Mixer<2>>(); mixer_music = make_shared<Mixer<2>>();
mixer_voice = make_shared<Mixer<2>>();
music_player = make_shared<MusicPlayer>(); music_player = make_shared<MusicPlayer>();
resample_music_player = make_shared<Resampler<2>>(music_player, 1.f); resample_music_player = make_shared<Resampler<2>>(music_player, 1.f);
gain_sound_effects = make_shared<Gain<2>>(); gain_sound_effects = make_shared<Gain<2>>();
gain_music_player = make_shared<Gain<2>>(); gain_music_player = make_shared<Gain<2>>();
gain_music_channel = make_shared<Gain<2>>(); gain_music_channel = make_shared<Gain<2>>();
gain_voice_channel = make_shared<Gain<2>>();
gain_sound_effects->bind(mixer_sound_effects); gain_sound_effects->bind(mixer_sound_effects);
gain_music_player->bind(resample_music_player); gain_music_player->bind(resample_music_player);
gain_music_channel->bind(mixer_music); gain_music_channel->bind(mixer_music);
gain_voice_channel->bind(mixer_voice);
master->add_source(gain_sound_effects); master->add_source(gain_sound_effects);
master->add_source(gain_music_channel); master->add_source(gain_music_channel);
master->add_source(gain_voice_channel);
mixer_music->add_source(gain_music_player); mixer_music->add_source(gain_music_player);
sound_effect_channels.clear(); sound_effect_channels.clear();
for (size_t i = 0; i < static_cast<size_t>(cv_numChannels.value); i++) for (size_t i = 0; i < static_cast<size_t>(cv_numChannels.value); i++)
@ -222,6 +352,13 @@ void initialize_sound()
sound_effect_channels.push_back(player); sound_effect_channels.push_back(player);
mixer_sound_effects->add_source(player); mixer_sound_effects->add_source(player);
} }
player_voice_channels.clear();
for (size_t i = 0; i < MAXPLAYERS; i++)
{
shared_ptr<SdlVoiceStreamPlayer> player = make_shared<SdlVoiceStreamPlayer>();
player_voice_channels.push_back(player);
mixer_voice->add_source(player);
}
} }
sound_started = true; sound_started = true;
@ -237,7 +374,17 @@ void I_StartupSound(void)
void I_ShutdownSound(void) void I_ShutdownSound(void)
{ {
SDL_CloseAudio(); if (g_device_id)
{
SDL_CloseAudioDevice(g_device_id);
g_device_id = 0;
}
if (g_input_device_id)
{
SDL_CloseAudioDevice(g_input_device_id);
g_input_device_id = 0;
}
SDL_QuitSubSystem(SDL_INIT_AUDIO); SDL_QuitSubSystem(SDL_INIT_AUDIO);
sound_started = false; sound_started = false;
@ -380,6 +527,17 @@ void I_SetSfxVolume(int volume)
} }
} }
void I_SetVoiceVolume(int volume)
{
SdlAudioLockHandle _;
float vol = static_cast<float>(volume) / 100.f;
if (gain_voice_channel)
{
gain_voice_channel->gain(std::clamp(vol * vol * vol, 0.f, 1.f));
}
}
void I_SetMasterVolume(int volume) void I_SetMasterVolume(int volume)
{ {
SdlAudioLockHandle _; SdlAudioLockHandle _;
@ -824,3 +982,93 @@ void I_UpdateAudioRecorder(void)
av_recorder = g_av_recorder; av_recorder = g_av_recorder;
#endif #endif
} }
boolean I_SoundInputIsEnabled(void)
{
return g_input_device_id != 0 && !g_input_device_paused;
}
boolean I_SoundInputSetEnabled(boolean enabled)
{
if (g_input_device_id == 0 && enabled)
{
SDL_AudioSpec input_desired {};
input_desired.format = AUDIO_F32SYS;
input_desired.channels = 1;
input_desired.samples = 2048;
input_desired.freq = 48000;
SDL_AudioSpec input_obtained {};
g_input_device_id = SDL_OpenAudioDevice(nullptr, SDL_TRUE, &input_desired, &input_obtained, 0);
if (!g_input_device_id)
{
CONS_Alert(CONS_WARNING, "Failed to open input audio device: %s\n", SDL_GetError());
return false;
}
g_input_device_paused = true;
}
if (enabled && g_input_device_paused)
{
SDL_PauseAudioDevice(g_input_device_id, SDL_FALSE);
g_input_device_paused = false;
}
else if (!enabled && !g_input_device_paused)
{
SDL_PauseAudioDevice(g_input_device_id, SDL_TRUE);
SDL_ClearQueuedAudio(g_input_device_id);
g_input_device_paused = true;
}
return !g_input_device_paused;
}
UINT32 I_SoundInputDequeueSamples(void *data, UINT32 len)
{
if (!g_input_device_id)
{
return 0;
}
UINT32 avail = SDL_GetQueuedAudioSize(g_input_device_id);
if (avail == 0)
{
return 0;
}
UINT32 ret = SDL_DequeueAudio(g_input_device_id, data, std::min(len, avail));
return ret;
}
void I_QueueVoiceFrameFromPlayer(INT32 playernum, void *data, UINT32 len, boolean terminal)
{
if (!sound_started)
{
return;
}
SdlAudioLockHandle _;
SdlVoiceStreamPlayer* player = player_voice_channels.at(playernum).get();
player->stream().put(tcb::span((std::byte*)data, len));
}
void I_SetPlayerVoiceProperties(INT32 playernum, float volume, float sep)
{
if (!sound_started)
{
return;
}
SdlAudioLockHandle _;
SdlVoiceStreamPlayer* player = player_voice_channels.at(playernum).get();
player->set_properties(volume * volume * volume, sep);
}
void I_ResetVoiceQueue(INT32 playernum)
{
if (!sound_started)
{
return;
}
SdlAudioLockHandle _;
SdlVoiceStreamPlayer* player = player_voice_channels.at(playernum).get();
player->stream().clear();
}

View file

@ -85,6 +85,7 @@ TYPEDEF (resultsall_pak);
TYPEDEF (say_pak); TYPEDEF (say_pak);
TYPEDEF (reqmapqueue_pak); TYPEDEF (reqmapqueue_pak);
TYPEDEF (netinfo_pak); TYPEDEF (netinfo_pak);
TYPEDEF (voice_pak);
// d_event.h // d_event.h
TYPEDEF (event_t); TYPEDEF (event_t);

View file

@ -799,6 +799,39 @@ void Y_PlayerStandingsDrawer(y_data_t *standings, INT32 xoffset)
player_names[pnum] player_names[pnum]
); );
{
patch_t *voxpat;
int voxxoffs = 0;
int voxyoffs = 0;
if (players[pnum].pflags2 & (PF2_SELFDEAFEN | PF2_SERVERDEAFEN))
{
voxpat = (patch_t*) W_CachePatchName("VOXCRD", PU_HUDGFX);
voxxoffs = 1;
voxyoffs = -5;
}
else if (players[pnum].pflags2 & (PF2_SELFMUTE | PF2_SERVERMUTE))
{
voxpat = (patch_t*) W_CachePatchName("VOXCRM", PU_HUDGFX);
voxxoffs = 1;
voxyoffs = -6;
}
else if (S_IsPlayerVoiceActive(pnum))
{
voxpat = (patch_t*) W_CachePatchName("VOXCRA", PU_HUDGFX);
voxyoffs = -4;
}
else
{
voxpat = NULL;
}
if (voxpat)
{
int namewidth = V_ThinStringWidth(player_names[pnum], 0);
V_DrawFixedPatch((x + 27 + namewidth + voxxoffs) * FRACUNIT, (y + voxyoffs) * FRACUNIT, FRACUNIT, 0, voxpat, NULL);
}
}
V_DrawRightAlignedThinString( V_DrawRightAlignedThinString(
x+118, y-2, x+118, y-2,
0, 0,

View file

@ -10,6 +10,7 @@
"libvpx", "libvpx",
"libvorbis", "libvorbis",
"libyuv", "libyuv",
"opus",
"zlib" "zlib"
], ],
"builtin-baseline": "c591ac6466a55ef0a05a3d56bb1489ca36e50102" "builtin-baseline": "c591ac6466a55ef0a05a3d56bb1489ca36e50102"