Merge branch 'theevolk-feature-soundapi' of github.com:ShadowOfEVolk/xash3d-fwgs

This commit is contained in:
Alibek Omarov
2026-04-08 17:18:03 +05:00
8 changed files with 293 additions and 126 deletions

185
common/sound_api.h Normal file
View File

@@ -0,0 +1,185 @@
/*
sound_api.h - Xash3D extension for client sound interface
Copyright (C) 2026 Xash3D FWGS Developers
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
*/
#ifndef SOUND_API_H
#define SOUND_API_H
#include "xash3d_types.h"
// Experimental implementation, backward compatibility is not guaranteed.
#define CL_SOUND_INTERFACE_VERSION 1
#define FL_VOXWORD_IN_CACHE BIT( 0 ) // if set, it was loaded prior and shouldn't be freed
#define FL_CHAN_USE_LOOP BIT( 0 ) // don't loop default and local sounds
#define FL_CHAN_STATIC_SOUND BIT( 1 ) // use origin instead of fetching entnum's origin
#define FL_CHAN_LOCAL_SOUND BIT( 2 ) // it's a local menu sound (not looped, not paused)
#define FL_CHAN_SENTENCE_FINISHED BIT( 4 ) // if set, finished playing sentence
#define FL_CHAN_FINISHED BIT( 5 ) // if set, finished playing single word
typedef int sound_t;
typedef struct portable_samplepair_s
{
int left;
int right;
} portable_samplepair_t;
typedef struct sfx_s
{
char name[MAX_QPATH];
wavdata_t *cache;
int servercount;
uint hashValue;
struct sfx_s *hashNext;
} sfx_t;
typedef struct voxword_s
{
sfx_t *sfx;
uint16_t volume; // volume percent
uint16_t pitch; // pitch shift percent (keep large for extra chipmunk fun)
uint8_t timecompress; // percent of skipped data (speeds up playback without pitch shift)
uint8_t start; // percent at which playback starts
uint8_t end; // percent at which playback ends
uint8_t flags;
} voxword_t;
typedef struct channel_s
{
char name[16]; // keep sentence name
sfx_t *sfx; // sfx number
vec3_t origin; // only use if fixed_origin is set
float dist_mult; // distance multiplier (attenuation/clipK)
int entchannel; // sound channel (CHAN_STREAM, CHAN_VOICE, etc.)
uint flags;
short entnum; // entity soundsource
short master_vol; // 0-255 master volume
short leftvol; // 0-255 left volume
short rightvol; // 0-255 right volume
short basePitch; // base pitch percent (100% is normal pitch playback)
byte word_index;
// HACKHACK: count when this channel became inaudible
// to not free it when it could be respatialized soon
#define MAX_CHANNEL_INAUDIBLE_TIME 0.1f
float inauduble_free_time;
double sample;
double forced_end;
wavdata_t *data;
voxword_t *words; // dynamically allocated, (num_words + 1) entries, null sfx terminates
uintptr_t engine_reserved[8]; // only for engine developers
uintptr_t game_reserved[8]; // free space for game developers
} channel_t;
typedef struct rawchan_s
{
short entnum;
short master_vol;
short leftvol; // 0-255 left volume
short rightvol; // 0-255 right volume
float dist_mult; // distance multiplier (attenuation/clipK)
vec3_t origin; // only use if fixed_origin is set
volatile uint s_rawend;
float oldtime; // catch time jumps
uintptr_t engine_reserved[8]; // only for engine developers
uintptr_t game_reserved[8]; // free space for game developers
size_t max_samples; // buffer length
portable_samplepair_t rawsamples[]; // variable sized
} rawchan_t;
typedef struct snd_format_s
{
uint speed;
byte width;
byte channels;
} snd_format_t;
typedef struct snd_globals_s
{
// dma
const char *backend_name;
byte *buffer;
snd_format_t format;
qboolean initialized; // sound engine is active
int samples; // mono samples in buffer
int samplepos; // in mono samples
int paintedtime; // total samples that have been mixed at speed
int soundtime; // total samples that have been played out to hardware at dma speed
// listener (client, camera, etc)
vec3_t origin;
vec3_t forward, right, up;
int entnum;
qboolean streaming; // playing AVI-file
qboolean stream_paused; // pause only background track
// SoundAPI shared pointers
channel_t *const channels;
int max_channels;
int total_channels;
rawchan_t **const raw_channels;
int max_raw_channels;
sound_t ambient_sfx[NUM_AMBIENTS];
qboolean have_ambient_sfx;
} snd_globals_t;
// API from engine to client (client calls these)
typedef struct sound_api_s
{
qboolean (*CL_GetEntitySpatialization)( channel_t *ch );
sfx_t* (*S_GetSfxByHandle)( sound_t handle );
} sound_api_t;
// Callbacks from client to engine (engine calls these when custom sound is active)
typedef struct sound_interface_s
{
int version;
qboolean (*pfnS_Init)( snd_globals_t *globals );
void (*pfnS_Shutdown)( void );
void (*pfnS_UpdateSound)( void );
/* Full paint: endtime (sample pairs), dma buffer, paintedtime in/out. Client does mix + transfer to dma.buffer. */
void (*pfnS_PaintChannels)( int endtime );
void (*pfnS_UpdateChannel)( int ch_idx, const channel_t *ch, sound_t handle ); // ch=NULL -> channel freed
void (*pfnS_UpdateRawChannel)( int raw_idx, rawchan_t *ch ); // ch=NULL -> channel freed
void (*pfnS_Spatialize)( channel_t *ch );
void (*pfnS_FreeSound)( sfx_t *sfx, sound_t handle );
} sound_interface_t;
#endif // SOUND_API_H

View File

@@ -23,6 +23,12 @@ struct mstudioevent_s;
struct engine_studio_api_s;
struct r_studio_interface_s;
struct sound_api_s;
struct sound_interface_s;
typedef struct sound_api_s sound_api_t;
typedef struct sound_interface_s sound_interface_t;
// NOTE: ordering is important!
typedef struct cldll_func_s
{
@@ -74,6 +80,8 @@ typedef struct cldll_func_s
int (*pfnTouchEvent)( int type, int fingerID, float x, float y, float dx, float dy );
void (*pfnMoveEvent)( float forwardmove, float sidemove );
void (*pfnLookEvent)( float relyaw, float relpitch );
// Sound API
int (*pfnGetSoundInterface)( int version, const sound_api_t *api, sound_interface_t *callback );
} cldll_func_t;
#endif//CDLL_EXP_H

View File

@@ -105,6 +105,7 @@ static const dllfunc_t cdll_new_exports[] = // allowed only in SDK 2.3 and high
{ "IN_ClientTouchEvent", (void **)&clgame.dllFuncs.pfnTouchEvent}, // Xash3D FWGS ext
{ "IN_ClientMoveEvent", (void **)&clgame.dllFuncs.pfnMoveEvent}, // Xash3D FWGS ext
{ "IN_ClientLookEvent", (void **)&clgame.dllFuncs.pfnLookEvent}, // Xash3D FWGS ext
{ "HUD_GetSoundInterface", (void **)&clgame.dllFuncs.pfnGetSoundInterface }, // Xash3D ext
};
static void pfnSPR_DrawHoles( int frame, int x, int y, const wrect_t *prc );

View File

@@ -3678,8 +3678,6 @@ void CL_Init( void )
CL_InitLocal();
VID_Init(); // init video
S_Init(); // init sound
Voice_Init( VOICE_DEFAULT_CODEC, 3, true ); // init voice (do not open the device)
// unreliable buffer. unsed for unreliable commands and voice stream
MSG_Init( &cls.datagram, "cls.datagram", cls.datagram_buf, sizeof( cls.datagram_buf ));
@@ -3689,6 +3687,9 @@ void CL_Init( void )
if( !CL_LoadProgs( libpath ))
Host_Error( "can't initialize %s: %s\n", libpath, COM_GetLibraryError( ));
S_Init(); // init sound
Voice_Init( VOICE_DEFAULT_CODEC, 3, true ); // init voice (do not open the device)
ID_Init();
SteamBroker_Init();

View File

@@ -26,6 +26,7 @@ GNU General Public License for more details.
#include "pm_defs.h"
#include "ref_params.h"
#include "render_api.h"
#include "sound_api.h"
#include "cdll_exp.h"
#include "screenfade.h"
#include "protocol.h"
@@ -454,6 +455,7 @@ typedef struct
void *hInstance; // pointer to client.dll
cldll_func_t dllFuncs; // dll exported funcs
render_interface_t drawFuncs; // custom renderer support
sound_interface_t soundFuncs; // custom sound support
poolhandle_t mempool; // client edicts pool
string mapname; // map name
string maptitle; // display map title

View File

@@ -232,6 +232,12 @@ void S_FreeSound( sfx_t *sfx )
prev = &hashSfx->hashNext;
}
if( clgame.soundFuncs.pfnS_FreeSound )
{
clgame.soundFuncs.pfnS_FreeSound( sfx, sfx - s_knownSfx );
return;
}
if( sfx->cache )
FS_FreeSound( sfx->cache );
memset( sfx, 0, sizeof( *sfx ));

View File

@@ -159,6 +159,18 @@ static qboolean S_IsClient( int entnum )
return entnum == snd.entnum;
}
/*
=================
S_NotifyChannelUpdate
=================
*/
static void S_NotifyChannelUpdate( int ch_idx, const channel_t *ch, sound_t handle )
{
if( !clgame.soundFuncs.pfnS_UpdateChannel )
return;
clgame.soundFuncs.pfnS_UpdateChannel( ch_idx, ch, handle );
}
// free channel so that it may be allocated by the
// next request to play a sound. If sound is a
@@ -171,6 +183,8 @@ S_FreeChannel
*/
void S_FreeChannel( channel_t *ch )
{
S_NotifyChannelUpdate( ch - snd.channels, NULL, -1 );
// free the currently loaded word's audio cache before nuking the channel
if( ch->words )
VOX_FreeWord( ch );
@@ -185,6 +199,19 @@ void S_FreeChannel( channel_t *ch )
SND_CloseMouth( ch );
}
/*
=================
S_NotifyRawChannelUpdate
=================
*/
static void S_NotifyRawChannelUpdate( int raw_idx, rawchan_t *ch )
{
if( !clgame.soundFuncs.pfnS_UpdateRawChannel )
return;
clgame.soundFuncs.pfnS_UpdateRawChannel( raw_idx, ch );
}
/*
=================
S_UpdateSoundFade
@@ -531,6 +558,12 @@ SND_Spatialize
*/
static void SND_Spatialize( channel_t *ch )
{
if( clgame.soundFuncs.pfnS_Spatialize )
{
clgame.soundFuncs.pfnS_Spatialize( ch );
return;
}
// anything coming from the view entity will allways be full volume
if( S_IsClient( ch->entnum ))
{
@@ -700,6 +733,8 @@ void S_StartSound( const vec3_t pos, int ent, int chan, sound_t handle, float fv
}
}
S_NotifyChannelUpdate( target_chan - snd.channels, target_chan, handle );
// Init client entity mouth movement vars
SND_InitMouth( ent, chan );
}
@@ -814,6 +849,8 @@ void S_RestoreSound( const vec3_t pos, int ent, int chan, sound_t handle, float
target_chan->sample = sample;
target_chan->forced_end = end;
S_NotifyChannelUpdate( target_chan - snd.channels, target_chan, handle );
// Init client entity mouth movement vars
SND_InitMouth( ent, chan );
}
@@ -907,6 +944,7 @@ void S_AmbientSound( const vec3_t pos, int ent, sound_t handle, float fvol, floa
ch->entchannel = CHAN_STATIC;
ch->basePitch = pitch;
S_NotifyChannelUpdate( ch - snd.channels, ch, handle );
SND_Spatialize( ch );
}
@@ -1173,6 +1211,8 @@ rawchan_t *S_FindRawChannel( int entnum, qboolean create )
ch->entnum = entnum;
ch->s_rawend = 0;
S_NotifyRawChannelUpdate( best, ch );
return ch;
}
@@ -1522,7 +1562,10 @@ static void S_UpdateChannels( void )
endtime -= ( endtime - snd.paintedtime ) & 0x3;
}
S_PaintChannels( endtime );
if( clgame.soundFuncs.pfnS_PaintChannels )
clgame.soundFuncs.pfnS_PaintChannels( endtime );
else
S_PaintChannels( endtime );
SNDDMA_Submit();
}
@@ -1568,6 +1611,9 @@ void SND_UpdateSound( void )
{
if( !snd.initialized ) return;
if( clgame.soundFuncs.pfnS_UpdateSound )
clgame.soundFuncs.pfnS_UpdateSound();
// if the loading plaque is up, clear everything
// out to make sure we aren't looping a dirty
// dma buffer while loading
@@ -1889,6 +1935,38 @@ static void S_VoiceRecordStop_f( void )
Voice_RecordStop();
}
static const sound_api_t gSoundAPI = {
CL_GetEntitySpatialization,
S_GetSfxByHandle,
};
/*
================
S_InitSoundAPI
================
*/
static qboolean S_InitSoundAPI( void )
{
// make sure what sound functions is cleared
memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs ));
if( clgame.dllFuncs.pfnGetSoundInterface )
{
if( clgame.dllFuncs.pfnGetSoundInterface( CL_SOUND_INTERFACE_VERSION, &gSoundAPI, &clgame.soundFuncs ))
{
Con_Reportf( "%s: ^2initailized extended SoundAPI ^7ver. %i\n", __func__, CL_SOUND_INTERFACE_VERSION );
return true;
}
Con_Reportf( "%s: ^1failed to initialize extended SoundAPI ^7ver. %i\n", __func__, CL_SOUND_INTERFACE_VERSION );
// make sure what sound functions is cleared
memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs ));
}
return false;
}
/*
================
S_Init
@@ -1949,6 +2027,11 @@ qboolean S_Init( void )
S_InitSounds ();
VOX_Init ();
S_InitSoundAPI();
if( clgame.soundFuncs.pfnS_Init )
clgame.soundFuncs.pfnS_Init( &snd );
return true;
}
@@ -1973,6 +2056,9 @@ void S_Shutdown( void )
Cmd_RemoveCommand( "speak" );
Cmd_RemoveCommand( "spk" );
if( clgame.soundFuncs.pfnS_Shutdown )
clgame.soundFuncs.pfnS_Shutdown();
S_StopAllSounds (false);
S_FreeRawChannels ();
S_FreeSounds ();

View File

@@ -19,6 +19,7 @@ GNU General Public License for more details.
extern poolhandle_t sndpool;
#include "xash3d_mathlib.h"
#include "sound_api.h"
// sound engine rate defines
#define SOUND_11k 11025 // 11khz sample rate
@@ -38,129 +39,6 @@ extern poolhandle_t sndpool;
#define CLIP16( x ) bound( SHRT_MIN + 8, x, SHRT_MAX - 8 )
typedef struct
{
int left;
int right;
} portable_samplepair_t;
typedef struct sfx_s
{
char name[MAX_QPATH];
wavdata_t *cache;
int servercount;
uint hashValue;
struct sfx_s *hashNext;
} sfx_t;
#define FL_VOXWORD_IN_CACHE BIT( 0 ) // if set, it was loaded prior and shouldn't be freed
typedef struct voxword_s
{
sfx_t *sfx;
uint16_t volume; // volume percent
uint16_t pitch; // pitch shift percent (keep large for extra chipmunk fun)
uint8_t timecompress; // percent of skipped data (speeds up playback without pitch shift)
uint8_t start; // percent at which playback starts
uint8_t end; // percent at which playback ends
uint8_t flags;
} voxword_t;
typedef struct snd_format_s
{
uint speed;
byte width;
byte channels;
} snd_format_t;
typedef struct rawchan_s
{
short entnum;
short master_vol;
short leftvol; // 0-255 left volume
short rightvol; // 0-255 right volume
float dist_mult; // distance multiplier (attenuation/clipK)
vec3_t origin; // only use if fixed_origin is set
volatile uint s_rawend;
float oldtime; // catch time jumps
uintptr_t engine_reserved[8]; // only for engine developers
uintptr_t game_reserved[8]; // free space for game developers
size_t max_samples; // buffer length
portable_samplepair_t rawsamples[]; // variable sized
} rawchan_t;
#define FL_CHAN_USE_LOOP BIT( 0 ) // don't loop default and local sounds
#define FL_CHAN_STATIC_SOUND BIT( 1 ) // use origin instead of fetching entnum's origin
#define FL_CHAN_LOCAL_SOUND BIT( 2 ) // it's a local menu sound (not looped, not paused)
#define FL_CHAN_SENTENCE_FINISHED BIT( 4 ) // if set, finished playing sentence
#define FL_CHAN_FINISHED BIT( 5 ) // if set, finished playing single word
typedef struct channel_s
{
char name[16]; // keep sentence name
sfx_t *sfx; // sfx number
vec3_t origin; // only use if fixed_origin is set
float dist_mult; // distance multiplier (attenuation/clipK)
int entchannel; // sound channel (CHAN_STREAM, CHAN_VOICE, etc.)
uint flags;
short entnum; // entity soundsource
short master_vol; // 0-255 master volume
short leftvol; // 0-255 left volume
short rightvol; // 0-255 right volume
short basePitch; // base pitch percent (100% is normal pitch playback)
byte word_index;
// HACKHACK: count when this channel became inaudible
// to not free it when it could be respatialized soon
#define MAX_CHANNEL_INAUDIBLE_TIME 0.1f
float inauduble_free_time;
double sample;
double forced_end;
wavdata_t *data;
voxword_t *words; // dynamically allocated, (num_words + 1) entries, null sfx terminates
uintptr_t engine_reserved[8]; // only for engine developers
uintptr_t game_reserved[8]; // free space for game developers
} channel_t;
typedef int sound_t;
typedef struct snd_globals_s
{
// dma
const char *backend_name;
byte *buffer;
snd_format_t format;
qboolean initialized; // sound engine is active
int samples; // mono samples in buffer
int samplepos; // in mono samples
int paintedtime; // total samples that have been mixed at speed
int soundtime; // total samples that have been played out to hardware at dma speed
// listener (client, camera, etc)
vec3_t origin;
vec3_t forward, right, up;
int entnum;
qboolean streaming; // playing AVI-file
qboolean stream_paused; // pause only background track
// SoundAPI shared pointers
channel_t *const channels;
int max_channels;
int total_channels;
rawchan_t **const raw_channels;
int max_raw_channels;
sound_t ambient_sfx[NUM_AMBIENTS];
qboolean have_ambient_sfx;
} snd_globals_t;
//====================================================================
#define MAX_DYNAMIC_CHANNELS (60 + NUM_AMBIENTS)