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(SDL2 CONFIG REQUIRED)
find_package(CURL REQUIRED)
find_package(Opus REQUIRED)
# Use the one in thirdparty/fmt to guarantee a minimum version
#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 PNG::PNG)
target_link_libraries(SRB2SDL2 PRIVATE CURL::libcurl)
target_link_libraries(SRB2SDL2 PRIVATE Opus::opus)
if("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD")
target_link_libraries(SRB2SDL2 PRIVATE -lexecinfo)
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_digmusicvolume = Player("musicvolume", "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
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.)
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 VoiceMute_OnChange(void);
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");
//

File diff suppressed because it is too large Load diff

View file

@ -137,6 +137,8 @@ typedef enum
PT_REQMAPQUEUE, // Client requesting a roundqueue operation
PT_VOICE, // Voice packet for either side
NUMPACKETTYPE
} packettype_t;
@ -283,6 +285,7 @@ struct clientconfig_pak
#define SV_SPEEDMASK 0x03 // used to send kartspeed
#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 MAXFILENEEDED 915
@ -418,6 +421,22 @@ struct netinfo_pak
UINT32 delay[MAXPLAYERS+1];
} 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
//
@ -462,6 +481,7 @@ struct doomdata_t
resultsall_pak resultsall; // 1024 bytes. Also, you really shouldn't trust anything here.
say_pak say; // I don't care anymore.
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
} ATTRPACK;
@ -606,6 +626,7 @@ void SendKick(UINT8 playernum, UINT8 msg);
// Create any new ticcmds and broadcast to other players.
void NetKeepAlive(void);
void NetUpdate(void);
void NetVoiceUpdate(void);
void SV_StartSinglePlayerServer(INT32 dogametype, boolean donetgame);
boolean SV_SpawnServer(void);
@ -710,6 +731,7 @@ void HandleSigfail(const char *string);
void DoSayPacket(SINT8 target, UINT8 flags, UINT8 source, char *message);
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);
#ifdef __cplusplus

View file

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

View file

@ -1175,6 +1175,8 @@ enum {
WP_AUTOROULETTE = 1<<2,
WP_ANALOGSTICK = 1<<3,
WP_AUTORING = 1<<4,
WP_SELFMUTE = 1<<5,
WP_SELFDEAFEN = 1<<6
};
void WeaponPref_Send(UINT8 ssplayer)
@ -1196,6 +1198,15 @@ void WeaponPref_Send(UINT8 ssplayer)
if (cv_autoring[ssplayer].value)
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];
buf[0] = prefs;
buf[1] = cv_mindelay.value;
@ -1235,6 +1246,7 @@ size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum)
UINT8 prefs = READUINT8(p);
player->pflags &= ~(PF_KICKSTARTACCEL|PF_SHRINKME|PF_AUTOROULETTE|PF_AUTORING);
player->pflags2 &= ~(PF2_SELFMUTE | PF2_SELFDEAFEN);
if (prefs & WP_KICKSTARTACCEL)
player->pflags |= PF_KICKSTARTACCEL;
@ -1253,6 +1265,12 @@ size_t WeaponPref_Parse(const UINT8 *bufstart, INT32 playernum)
if (prefs & WP_AUTORING)
player->pflags |= PF_AUTORING;
if (prefs & WP_SELFMUTE)
player->pflags2 |= PF2_SELFMUTE;
if (prefs & WP_SELFDEAFEN)
player->pflags2 |= PF2_SELFDEAFEN;
if (leveltime < 2)
{
// 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);
}
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.
* A lot of code (written by dummies, obviously) uses COM_BufAddText() to run
* 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_mute;
extern consvar_t cv_voice_servermute;
extern consvar_t cv_pause;
extern consvar_t cv_restrictskinchange, cv_allowteamchange, cv_maxplayers;
@ -185,6 +186,8 @@ typedef enum
XD_CALLZVOTE, // 39
XD_SETZVOTE, // 40
XD_TEAMCHANGE, // 41
XD_SERVERMUTEPLAYER, // 42
XD_SERVERDEAFENPLAYER, // 43
MAXNETXCMD
} 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.
} pflags_t;
typedef enum
{
PF2_SELFMUTE = 1<<1,
PF2_SELFDEAFEN = 1<<2,
PF2_SERVERMUTE = 1<<3,
PF2_SERVERDEAFEN = 1<<4,
} pflags2_t;
typedef enum
{
// Are animation frames playing?
@ -634,6 +642,7 @@ struct player_t
// Bit flags.
// See pflags_t, above.
UINT32 pflags;
UINT32 pflags2;
// playing animation.
panim_t panim;
@ -1054,7 +1063,7 @@ struct player_t
UINT8 amps;
UINT8 amppickup;
UINT8 ampspending;
UINT16 overdrive;
UINT16 overshield;
fixed_t overdrivepower;

View file

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

View file

@ -224,3 +224,30 @@ boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
(void)looping;
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;
INT32 pflags;
INT32 pflags2;
UINT8 team;
@ -2348,6 +2349,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
xtralife = players[player].xtralife;
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
memcpy(&itemRoulette, &players[player].itemRoulette, sizeof (itemRoulette));
@ -2539,6 +2541,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps)
p->roundscore = roundscore;
p->lives = lives;
p->pflags = pflags;
p->pflags2 = pflags2;
p->team = team;
p->jointime = jointime;
p->splitscreenindex = splitscreenindex;
@ -5916,7 +5919,7 @@ boolean G_SameTeam(const player_t *a, const player_t *b)
}
// Free for all.
return false;
return false;
}
UINT8 G_CountTeam(UINT8 team)

View file

@ -910,6 +910,7 @@ static const char *gamecontrolname[num_gamecontrols] =
"screenshot",
"startmovie",
"startlossless",
"voicepushtotalk"
};
#define NUMKEYNAMES (sizeof (keynames)/sizeof (keyname_t))
@ -1332,7 +1333,7 @@ INT32 G_FindPlayerBindForGameControl(INT32 player, gamecontrols_e control)
}
}
return (bestbind != -1) ? bestbind : anybind; // If we couldn't find a device-appropriate bind, try to at least use something
return (bestbind != -1) ? bestbind : anybind; // If we couldn't find a device-appropriate bind, try to at least use something
}
static void setcontrol(UINT8 player)

View file

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

View file

@ -86,6 +86,7 @@ patch_t *frameslash; // framerate stuff. Used in screen.c
static player_t *plr;
boolean hu_keystrokes; // :)
boolean chat_on; // entering a chat message?
boolean g_voicepushtotalk_on; // holding PTT?
static char w_chat[HU_MAXMSGLEN + 1];
static size_t c_input = 0; // let's try to make the chat input less shitty.
static boolean headsupactive = false;
@ -1102,6 +1103,24 @@ void HU_clearChatChars(void)
//
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)
return false;
@ -1912,25 +1931,25 @@ static void HU_DrawTitlecardCEcho(size_t num)
{
INT32 ofs;
INT32 timer = (INT32)(elapsed - timeroffset);
if (timer <= 0)
return; // we don't care.
line = strchr(echoptr, '\\');
if (line == NULL)
break;
*line = '\0';
ofs = V_CenteredTitleCardStringOffset(echoptr, p4);
V_DrawTitleCardString(x - ofs, y, echoptr, 0, false, timer, fadeout, p4);
y += p4 ? 18 : 32;
// offset the timer for the next line.
timeroffset += strlen(echoptr);
// set the ptr to the \0 we made and advance it because we don't want an empty string.
echoptr = line;
echoptr++;

View file

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

View file

@ -27,6 +27,7 @@
#include "../k_hud.h"
#include "../p_local.h"
#include "../r_fps.h"
#include "../s_sound.h"
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_SetVoiceVolume(int volume);
/// ------------------------
// MUSIC SYSTEM
/// ------------------------
@ -246,6 +248,22 @@ boolean I_FadeSong(UINT8 target_volume, UINT32 ms, void (*callback)(void));
boolean I_FadeOutStopSong(UINT32 ms);
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
} // extern "C"
#endif

File diff suppressed because it is too large Load diff

View file

@ -346,6 +346,7 @@ typedef enum
mopt_profiles = 0,
mopt_video,
mopt_sound,
mopt_voice,
mopt_hud,
mopt_gameplay,
mopt_server,
@ -468,6 +469,9 @@ extern menu_t OPTIONS_VideoAdvancedDef;
extern menuitem_t OPTIONS_Sound[];
extern menu_t OPTIONS_SoundDef;
extern menuitem_t OPTIONS_Voice[];
extern menu_t OPTIONS_VoiceDef;
extern menuitem_t OPTIONS_HUD[];
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);
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 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);
}
}
// voice chat enabled
if (serverlist[i].info.kartvars & SV_VOICEENABLED)
{
V_DrawFixedPatch((startx - 3) * FRACUNIT, (starty + 2) * FRACUNIT, FRACUNIT, 0, voicepat, NULL);
}
}
ypos += SERVERSPACE;
}

View file

@ -310,10 +310,12 @@ void PR_SaveProfiles(void)
for (size_t j = 0; j < num_gamecontrols; j++)
{
std::vector<int32_t> mappings;
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));
@ -498,9 +500,24 @@ void PR_LoadProfiles(void)
{
for (size_t j = 0; j < num_gamecontrols; j++)
{
if (jsprof.controls.size() <= j)
{
for (size_t k = 0; k < MAXINPUTMAPPING; k++)
{
newprof->controls[j][k] = gamecontroldefault[j][k];
}
continue;
}
auto& mappings = jsprof.controls.at(j);
for (size_t k = 0; k < MAXINPUTMAPPING; k++)
{
newprof->controls[j][k] = jsprof.controls.at(j).at(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;
ProfileRecordsJson records;
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(
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();
}

View file

@ -58,6 +58,25 @@ static void K_MidVoteKick(void)
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)
@ -99,6 +118,13 @@ static midVoteTypeDef_t g_midVoteTypeDefs[MVT__MAX] =
K_MidVoteKick
},
{ // MVT_MUTE
"MUTE",
"Mute Player?",
CVAR_INIT ("zvote_mute_allowed", "Yes", CV_SAVE|CV_NETVAR, CV_YesNo, NULL),
K_MidVoteMute
},
{ // MVT_RTV
"RTV",
"Skip Level?",
@ -127,6 +153,10 @@ boolean K_MidVoteTypeUsesVictim(midVoteType_e voteType)
{
return true;
}
case MVT_MUTE:
{
return true;
}
default:
{
return false;
@ -1186,6 +1216,7 @@ void K_DrawMidVote(void)
switch (g_midVote.type)
{
case MVT_KICK:
case MVT_MUTE:
{
// Draw victim name
if (g_midVote.victim != NULL)

View file

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

View file

@ -35,18 +35,39 @@ extern "C" {
| \
(((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.
// WAD files are stored little endian.
#ifdef SRB2_BIG_ENDIAN
#define SHORT SWAP_SHORT
#define LONG SWAP_LONG
#define LONGLON SWAP_LONGLONG
#define MSBF_SHORT(x) ((INT16)(x))
#define MSBF_LONG(x) ((INT32)(x))
#define MSBF_LONGLONG(x) ((INT64)(x))
#else
#define SHORT(x) ((INT16)(x))
#define LONG(x) ((INT32)(x))
#define LONGLONG(x) ((INT64)(x))
#define MSBF_SHORT SWAP_SHORT
#define MSBF_LONG SWAP_LONG
#define MSBF_LONGLONG SWAP_LONGLONG
#endif
// Big to little endian

View file

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

View file

@ -31,6 +31,9 @@ menuitem_t OPTIONS_Main[] =
{IT_STRING | IT_CALL, "Sound Options", "Adjust the volume.",
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.",
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.",
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.",
NULL, {.routine = M_ProfileSetControl}, gc_lua1, 0},
@ -310,14 +313,14 @@ boolean M_ProfileControlsInputs(INT32 ch)
S_StartSound(NULL, sfx_kc69);
if (newbuttons & MBT_R)
S_StartSound(NULL, sfx_s3ka2);
if (newbuttons & MBT_A)
S_StartSound(NULL, sfx_kc3c);
if (newbuttons & MBT_B)
S_StartSound(NULL, sfx_3db09);
if (newbuttons & MBT_C)
S_StartSound(NULL, sfx_s1be);
if (newbuttons & MBT_X)
S_StartSound(NULL, sfx_s1a4);
if (newbuttons & MBT_Y)

View file

@ -29,7 +29,7 @@ menuitem_t OPTIONS_Server[] =
{IT_HEADER, "Players...", NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Maximum Players", "How many players can play at once.",
NULL, {.cvar = &cv_maxplayers}, 0, 0},
@ -40,7 +40,7 @@ menuitem_t OPTIONS_Server[] =
NULL, {.cvar = &cv_kartbot}, 0, 0},
{IT_STRING | IT_CVAR, "Use PWR.LV", "Should players should be rated on their performance?",
NULL, {.cvar = &cv_kartusepwrlv}, 0, 0},
NULL, {.cvar = &cv_kartusepwrlv}, 0, 0},
{IT_STRING | IT_CVAR, "Antigrief Timer (seconds)", "How long can players stop progressing before they're removed?",
NULL, {.cvar = &cv_antigrief}, 0, 0},
@ -78,6 +78,9 @@ menuitem_t OPTIONS_Server[] =
{IT_STRING | IT_CVAR, "Mute Chat", "Prevent everyone but admins from sending chat messages.",
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.",
NULL, {.cvar = &cv_chatspamprotection}, 0, 0},

View file

@ -38,6 +38,7 @@ struct Slider
kMasterVolume,
kMusicVolume,
kSfxVolume,
kVoiceVolume,
kNumSliders
};
@ -120,6 +121,7 @@ std::array<Slider, Slider::kNumSliders> sliders{{
n = !n;
CV_SetValue(&cv_gamedigimusic, n);
CV_SetValue(&cv_gamesounds, n);
CV_SetValue(&cv_voice_chat, n);
}
return n;
@ -150,6 +152,18 @@ std::array<Slider, Slider::kNumSliders> sliders{{
},
cv_soundvolume,
},
{
[](bool toggle) -> bool
{
if (toggle)
{
CV_AddValue(&cv_voice_chat, 1);
}
return !S_VoiceDisabled();
},
cv_voicevolume,
},
}};
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.",
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,
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.
void S_StopSounds(void)
{
@ -676,6 +684,13 @@ void S_StopSound(void *origin)
static INT32 actualsfxvolume; // check for change through console
static INT32 actualdigmusicvolume;
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)
{
@ -694,6 +709,8 @@ void S_UpdateSounds(void)
S_SetMusicVolume();
if (actualmastervolume != cv_mastervolume.value)
S_SetMasterVolume();
if (actualvoicevolume != cv_voicevolume.value)
S_SetVoiceVolume();
// We're done now, if we're not in a level.
if (gamestate != GS_LEVEL)
@ -853,6 +870,154 @@ notinlevel:
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)
{
UINT8 i;
@ -887,6 +1052,13 @@ void S_SetSfxVolume(void)
I_SetSfxVolume(actualsfxvolume);
}
void S_SetVoiceVolume(void)
{
actualvoicevolume = cv_voicevolume.value;
I_SetVoiceVolume(actualvoicevolume);
}
void S_SetMasterVolume(void)
{
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)
{
@ -2656,3 +2840,53 @@ void BGAudio_OnChange(void)
if (window_notinfocus && !(cv_bgaudio.value & 2))
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 cv_soundvolume, cv_closedcaptioning, cv_digmusicvolume;
extern consvar_t cv_voicevolume;
extern consvar_t surround;
extern consvar_t cv_numChannels;
@ -47,6 +48,22 @@ extern consvar_t cv_gamesounds;
extern consvar_t cv_bgaudio;
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
{
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_VoiceDisabled(void);
//
// 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_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);
@ -257,6 +277,7 @@ INT32 S_GetSoundVolume(sfxinfo_t *sfx, INT32 volume);
void S_SetSfxVolume(void);
void S_SetMusicVolume(void);
void S_SetMasterVolume(void);
void S_SetVoiceVolume(void);
INT32 S_OriginPlaying(void *origin);
INT32 S_IdPlaying(sfxenum_t id);
@ -270,6 +291,15 @@ void S_StopSoundByNum(sfxenum_t sfxnum);
#define S_StartAttackSound 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
} // extern "C"
#endif

View file

@ -53,6 +53,125 @@ using srb2::audio::Source;
using namespace srb2;
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
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>> mixer_sound_effects;
static shared_ptr<Mixer<2>> mixer_music;
static shared_ptr<Mixer<2>> mixer_voice;
static shared_ptr<MusicPlayer> music_player;
static shared_ptr<Resampler<2>> resample_music_player;
static shared_ptr<Gain<2>> gain_sound_effects;
static shared_ptr<Gain<2>> gain_music_player;
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<SdlVoiceStreamPlayer>> player_voice_channels;
#ifdef SRB2_CONFIG_ENABLE_WEBM_MOVIES
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 SDL_AudioDeviceID g_device_id;
static SDL_AudioDeviceID g_input_device_id;
static boolean g_input_device_paused;
void* I_GetSfx(sfxinfo_t* sfx)
{
if (sfx->lumpnum == LUMPERROR)
@ -120,8 +246,8 @@ namespace
class SdlAudioLockHandle
{
public:
SdlAudioLockHandle() { SDL_LockAudio(); }
~SdlAudioLockHandle() { SDL_UnlockAudio(); }
SdlAudioLockHandle() { SDL_LockAudioDevice(g_device_id); }
~SdlAudioLockHandle() { SDL_UnlockAudioDevice(g_device_id); }
};
#ifdef TRACY_ENABLE
@ -180,21 +306,21 @@ void initialize_sound()
return;
}
SDL_AudioSpec desired;
SDL_AudioSpec desired {};
desired.format = AUDIO_F32SYS;
desired.channels = 2;
desired.samples = cv_soundmixingbuffersize.value;
desired.freq = 44100;
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());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return;
}
SDL_PauseAudio(SDL_FALSE);
SDL_PauseAudioDevice(g_device_id, SDL_FALSE);
{
SdlAudioLockHandle _;
@ -204,16 +330,20 @@ void initialize_sound()
master_gain->bind(master);
mixer_sound_effects = make_shared<Mixer<2>>();
mixer_music = make_shared<Mixer<2>>();
mixer_voice = make_shared<Mixer<2>>();
music_player = make_shared<MusicPlayer>();
resample_music_player = make_shared<Resampler<2>>(music_player, 1.f);
gain_sound_effects = make_shared<Gain<2>>();
gain_music_player = 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_music_player->bind(resample_music_player);
gain_music_channel->bind(mixer_music);
gain_voice_channel->bind(mixer_voice);
master->add_source(gain_sound_effects);
master->add_source(gain_music_channel);
master->add_source(gain_voice_channel);
mixer_music->add_source(gain_music_player);
sound_effect_channels.clear();
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);
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;
@ -237,7 +374,17 @@ void I_StartupSound(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);
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)
{
SdlAudioLockHandle _;
@ -824,3 +982,93 @@ void I_UpdateAudioRecorder(void)
av_recorder = g_av_recorder;
#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 (reqmapqueue_pak);
TYPEDEF (netinfo_pak);
TYPEDEF (voice_pak);
// d_event.h
TYPEDEF (event_t);

View file

@ -799,6 +799,39 @@ void Y_PlayerStandingsDrawer(y_data_t *standings, INT32 xoffset)
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(
x+118, y-2,
0,

View file

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