diff --git a/common/sound_api.h b/common/sound_api.h new file mode 100644 index 00000000..d25c18be --- /dev/null +++ b/common/sound_api.h @@ -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 +*/ + +#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 diff --git a/engine/cdll_exp.h b/engine/cdll_exp.h index 240d9c25..0cd04636 100644 --- a/engine/cdll_exp.h +++ b/engine/cdll_exp.h @@ -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 diff --git a/engine/client/cl_game.c b/engine/client/cl_game.c index 914c79fa..2b9d7c43 100644 --- a/engine/client/cl_game.c +++ b/engine/client/cl_game.c @@ -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 ); diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index bc3ac478..f185f1e3 100644 --- a/engine/client/cl_main.c +++ b/engine/client/cl_main.c @@ -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(); diff --git a/engine/client/client.h b/engine/client/client.h index 0b7a8e67..d8003903 100644 --- a/engine/client/client.h +++ b/engine/client/client.h @@ -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 diff --git a/engine/client/s_load.c b/engine/client/s_load.c index bba2e84e..1d56b963 100644 --- a/engine/client/s_load.c +++ b/engine/client/s_load.c @@ -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 )); diff --git a/engine/client/s_main.c b/engine/client/s_main.c index af73cdc9..a4e38723 100644 --- a/engine/client/s_main.c +++ b/engine/client/s_main.c @@ -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 (); diff --git a/engine/client/sound.h b/engine/client/sound.h index e2a5c528..4c8ea4c7 100644 --- a/engine/client/sound.h +++ b/engine/client/sound.h @@ -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)