diff --git a/common/sound_api.h b/common/sound_api.h new file mode 100644 index 00000000..dcb0956e --- /dev/null +++ b/common/sound_api.h @@ -0,0 +1,67 @@ +/* +sound_api.h - Xash3D extension for client sound interface +Copyright (C) 2026 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#ifndef SOUND_API_H +#define SOUND_API_H + +#include "xash3d_types.h" + +#define CL_SOUND_INTERFACE_VERSION 1 + +// sound handle (same as engine's sound_t) +typedef int sound_t; + +struct dma_api_s; +struct listener_s; + +// Forward decls (full def in engine/client/sound.h) +struct channel_s; +struct rawchan_s; +struct portable_samplepair_s; + +// Engine-owned state: client reads only. Filled by engine, passed to pfnS_Init. +typedef struct snd_interface_state_s +{ + const struct listener_s *listener; + const struct channel_s *channels; // &channels[0], contiguous + int total_channels; + struct rawchan_s *const *raw_channels; // rawchan_t*[] + int max_raw_channels; +} snd_interface_state_t; + +// API from engine to client (client calls these) +typedef struct sound_api_s +{ + qboolean (*CL_GetEntitySpatialization)( struct channel_s *ch ); + struct sfx_s* (*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_interface_state_t *state ); + 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, struct dma_api_s *dma, int *paintedtime ); + void (*pfnS_UpdateChannel)( int ch_idx, const struct channel_s *ch, sound_t handle ); // ch=NULL -> channel freed + void (*pfnS_UpdateRawChannel)( int raw_idx, struct rawchan_s *ch ); // ch=NULL -> channel freed + void (*pfnS_Spatialize)( struct channel_s *ch ); + void (*pfnS_FreeSound)( struct sfx_s *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..3751b7e1 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, 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 592b705c..c86faad0 100644 --- a/engine/client/cl_game.c +++ b/engine/client/cl_game.c @@ -92,6 +92,7 @@ static const dllfunc_t cdll_new_exports[] = // allowed only in SDK 2.3 and high { "HUD_VoiceStatus", (void **)&clgame.dllFuncs.pfnVoiceStatus }, { "HUD_ChatInputPosition", (void **)&clgame.dllFuncs.pfnChatInputPosition }, { "HUD_GetRenderInterface", (void **)&clgame.dllFuncs.pfnGetRenderInterface }, // Xash3D ext +{ "HUD_GetSoundInterface", (void **)&clgame.dllFuncs.pfnGetSoundInterface }, // Xash3D ext { "HUD_ClipMoveToEntity", (void **)&clgame.dllFuncs.pfnClipMoveToEntity }, // Xash3D ext { "IN_ClientTouchEvent", (void **)&clgame.dllFuncs.pfnTouchEvent}, // Xash3D FWGS ext { "IN_ClientMoveEvent", (void **)&clgame.dllFuncs.pfnMoveEvent}, // Xash3D FWGS ext diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index 5c881008..3580bf4a 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 013edf37..ff8b861e 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" @@ -449,6 +450,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 4dbed9e3..0e094220 100644 --- a/engine/client/s_load.c +++ b/engine/client/s_load.c @@ -232,6 +232,11 @@ 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 b2f0888c..3b6f1282 100644 --- a/engine/client/s_main.c +++ b/engine/client/s_main.c @@ -45,6 +45,9 @@ int total_channels; int soundtime; // sample PAIRS int paintedtime; // sample PAIRS +static sound_api_t s_clientSoundAPI; +static snd_interface_state_t s_sndState; + static CVAR_DEFINE( s_volume, "volume", "0.7", FCVAR_ARCHIVE|FCVAR_FILTERABLE, "sound volume" ); CVAR_DEFINE( s_musicvolume, "MP3Volume", "1.0", FCVAR_ARCHIVE|FCVAR_FILTERABLE, "background music volume" ); static CVAR_DEFINE( s_mixahead, "_snd_mixahead", "0.12", FCVAR_FILTERABLE, "how much sound to mix ahead of time" ); @@ -161,6 +164,18 @@ static qboolean S_IsClient( int entnum ) return entnum == s_listener.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 @@ -173,6 +188,10 @@ S_FreeChannel */ void S_FreeChannel( channel_t *ch ) { + int ch_idx = ch - channels; + if( ch_idx >= 0 && ch_idx < MAX_CHANNELS ) + S_NotifyChannelUpdate( ch_idx, NULL, -1 ); + ch->sfx = NULL; ch->name[0] = '\0'; ch->use_loop = false; @@ -184,6 +203,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 @@ -543,6 +575,11 @@ 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 )) { @@ -704,6 +741,9 @@ void S_StartSound( const vec3_t pos, int ent, int chan, sound_t handle, float fv } } + ch_idx = target_chan - channels; + S_NotifyChannelUpdate( ch_idx, target_chan, handle ); + // Init client entity mouth movement vars SND_InitMouth( ent, chan ); } @@ -810,6 +850,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 - channels, target_chan, handle ); + // Init client entity mouth movement vars SND_InitMouth( ent, chan ); } @@ -898,6 +940,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 - channels, ch, handle ); SND_Spatialize( ch ); } @@ -1161,6 +1204,8 @@ rawchan_t *S_FindRawChannel( int entnum, qboolean create ) ch->entnum = entnum; ch->s_rawend = 0; + S_NotifyRawChannelUpdate( best, ch ); + return ch; } @@ -1513,7 +1558,11 @@ static void S_UpdateChannels( void ) endtime -= ( endtime - paintedtime ) & 0x3; } - S_PaintChannels( endtime ); + s_sndState.total_channels = total_channels; + if( clgame.soundFuncs.pfnS_PaintChannels ) + clgame.soundFuncs.pfnS_PaintChannels( endtime, &dma, &paintedtime ); + else + S_PaintChannels( endtime ); SNDDMA_Submit(); } @@ -1563,6 +1612,10 @@ void SND_UpdateSound( void ) if( !dma.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 @@ -1917,6 +1970,33 @@ static void S_VoiceRecordStop_f( void ) Voice_RecordStop(); } +/* +================ +S_FillSoundAPI +================ +*/ +static void S_FillSoundAPI( sound_api_t *api ) +{ + memset( api, 0, sizeof( *api )); + api->CL_GetEntitySpatialization = CL_GetEntitySpatialization; + api->S_GetSfxByHandle = S_GetSfxByHandle; +} + +/* +================ +S_FillSndState +================ +*/ +static void S_FillSndState( snd_interface_state_t *st ) +{ + memset( st, 0, sizeof( *st )); + st->listener = &s_listener; + st->channels = channels; + st->total_channels = total_channels; + st->raw_channels = raw_channels; + st->max_raw_channels = MAX_RAW_CHANNELS; +} + /* ================ S_Init @@ -1978,6 +2058,22 @@ qboolean S_Init( void ) S_InitSounds (); VOX_Init (); + memset( &clgame.soundFuncs, 0, sizeof( clgame.soundFuncs )); + + if( clgame.dllFuncs.pfnGetSoundInterface ) + { + S_FillSoundAPI( &s_clientSoundAPI ); + S_FillSndState( &s_sndState ); + + if( clgame.dllFuncs.pfnGetSoundInterface( CL_SOUND_INTERFACE_VERSION, &s_clientSoundAPI, &clgame.soundFuncs )) + { + if( clgame.soundFuncs.pfnS_Init && clgame.soundFuncs.pfnS_Init( &s_sndState )) + { + Con_Reportf( "%s: ^2initialized extended SoundAPI ^7ver. %i\n", __func__, CL_SOUND_INTERFACE_VERSION ); + } + } + } + return true; } @@ -2002,6 +2098,11 @@ 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 47962b13..dea7fb5d 100644 --- a/engine/client/sound.h +++ b/engine/client/sound.h @@ -38,7 +38,7 @@ extern poolhandle_t sndpool; #define CLIP16( x ) bound( SHRT_MIN + 8, x, SHRT_MAX - 8 ) -typedef struct +typedef struct portable_samplepair_s { int left; int right; @@ -72,7 +72,7 @@ typedef struct snd_format_s byte channels; } snd_format_t; -typedef struct +typedef struct dma_api_s { snd_format_t format; int samples; // mono samples in buffer