Sounds
Play sound events for exactly the players you pick, stop or change them by guid, give players volume channels, and rewrite the game's own sounds.
ISource2ModSounds builds and sends the sound event network message itself
instead of going through an entity on the server. That gives you four things:
- a sound goes to exactly the recipients you pick — nobody else receives it;
- every emit returns a guid that stops the sound or changes it while it plays;
- volume channels: each player can have their own volume for
"music","effects"or any name you choose, applied per recipient; - every sound the game sends passes through a hook where it can be blocked, renamed, re-parametrised or re-addressed.
#include "source2mod/ISource2ModSounds.h"#include <source2mod/api>One-call emits
For the common case there is one call per audience. All of them return the
SoundGuid of the new sound (0 when nobody received it).
SoundGuid EmitSoundToPlayer(CPlayerSlot slot, const char* name, float volume = 1.0f, float pitch = 1.0f, const char* channel = nullptr);
SoundGuid EmitSoundToPlayers(SoundRecipients recipients, const char* name, float volume = 1.0f, float pitch = 1.0f, const char* channel = nullptr);
SoundGuid EmitSoundToAll(const char* name, float volume = 1.0f, float pitch = 1.0f, const char* channel = nullptr);
SoundGuid EmitSoundFromEntity(CEntityInstance* entity, const char* name, float volume = 1.0f, float pitch = 1.0f,
SoundRecipients recipients = ~SoundRecipients(0), const char* channel = nullptr);A script calls the same four on Source2ModSounds (Source2ModSounds.EmitSoundToPlayer(slot, name)
and so on), with the same defaults; a guid is an int.
The first three play "in the head" of each recipient — no position, no falloff. From the sample plugin:
REGISTER_CON_COMMAND("sample_sound", [](const Source2ModCommandContext& context, const Source2ModCommandArgs& args, bool post)
{
const CPlayerSlot slot = context.GetPlayerSlot();
const char* pszSound = args.ArgC() >= 2 ? args.Arg(1) : "Weapon_AK47.Single";
if (slot.IsValid())
SOUND_EMIT_TO_PLAYER(slot, pszSound);
else
SOUND_EMIT_TO_ALL(pszSound);
});public void OnPluginStart()
{
Source2ModCommands.RegisterConCommand("sample_sound", Cmd_Sound);
}
public void Cmd_Sound(Source2ModCommandContext context, Source2ModCommandArgs args, bool post)
{
char sound[128] = "Weapon_AK47.Single";
if (args.ArgC() >= 2)
args.Arg(1, sound, sizeof(sound));
CPlayerSlot slot = context.GetPlayerSlot();
if (slot.IsValid())
Source2ModSounds.EmitSoundToPlayer(slot, sound);
else
Source2ModSounds.EmitSoundToAll(sound);
}SoundRecipients is a uint64_t with one bit per player slot.
SoundRecipientOf(slot) gives the mask of one slot; OR them together for a
group. In a script the mask is an int[2] (slots 0–31 and 32–63), built with
PlayerMask_Add(mask, slot); {-1, -1} is everybody.
Sound objects
For anything more — a position, extra parameters, a hand-picked recipient
list — create an ISource2ModSound, configure it and call Emit(). Release it
with delete (or g_pSource2ModSounds->DestroySound()); an object can be emitted
any number of times, each call a new sound with a new guid.
ISource2ModSound* pSound = ISource2ModSound::New("Weapon_AK47.Single");
pSound->SetVolume(0.5f);
pSound->SetPitch(2.0f);
// One of three sources:
// SetSourceEntityIndex(SOUND_SOURCE_LISTENER) -- at each recipient (default)
// SetPosition(vec) -- a fixed point in the world
// SetSourceEntity(pEntity) -- from an entity, following it
pSound->SetPosition(pPawn->GetAbsOrigin() + Vector(0.0f, 0.0f, 128.0f));
// Any other public parameter of the sound event, by name:
// pSound->SetFloat("public.my_param", 1.0f);
pSound->AddAllRecipients();
pSound->RemoveRecipient(CPlayerSlot(63));
const SoundGuid guid = pSound->Emit();
delete pSound;ISource2ModSound sound = ISource2ModSound.New("Weapon_AK47.Single");
sound.SetVolume(0.5);
sound.SetPitch(2.0);
// One of three sources:
// SetSourceEntityIndex(SOUND_SOURCE_LISTENER) -- at each recipient (default)
// SetPosition(vec) -- a fixed point in the world
// SetSourceEntity(entity) -- from an entity, following it
float position[3];
pawn.GetAbsOrigin(position);
position[2] += 128.0;
sound.SetPosition(position);
// Any other public parameter of the sound event, by name:
// sound.SetFloat("public.my_param", 1.0);
sound.AddAllRecipients();
sound.RemoveRecipient(CPlayerSlot(63));
int guid = sound.Emit();
delete sound;Typed parameter setters exist for every known wire type: SetBool,
SetInt32, SetUInt32, SetUInt64, SetFloat, SetFloat3, with matching
getters that take a default (in a script SetUInt64 takes two cells, low
and high). Names are the sound event's public fields
("public.volume", "public.pitch", "public.position", or whatever a custom
.vsndevts declares).
Recipients: AddRecipient(slot), RemoveRecipient(slot), HasRecipient(slot),
AddAllRecipients() (everyone connected at the time of the call),
ClearRecipients(), SetRecipients(mask), GetRecipients().
Stopping and changing a playing sound
The guid is the handle to a sound that is already playing on the clients.
ADD_TIMER(0.1f, [guid]() { g_pSource2ModSounds->SetSoundVolume(guid, 0.1f); });
ADD_TIMER(0.3f, [guid]() { SOUND_STOP(guid); });Source2ModScheduler.AddTimer(0.1, Timer_QuietSound, guid);
Source2ModScheduler.AddTimer(0.3, Timer_StopSound, guid);
public void Timer_QuietSound(any guid)
{
Source2ModSounds.SetSoundVolume(guid, 0.1);
}
public void Timer_StopSound(any guid)
{
Source2ModSounds.StopSound(guid);
}| Call | Effect |
|---|---|
StopSound(guid) | Stops it for everyone it was sent to |
StopSoundForPlayers(guid, recipients) | Stops it for some; it keeps playing for the rest |
StopSoundByName(name, sourceEntityIndex = SOUND_SOURCE_LISTENER, recipients = ~SoundRecipients(0)) | Stops by name — also works for a sound the game started |
SetSoundVolume(guid, volume) | New volume; channel volumes still apply |
UpdateSoundParams(guid, params, recipients = SOUND_RECIPIENTS_NONE) | Sends the parameters set on params (an ISource2ModSound) to the playing sound; 0 recipients means everyone it went to |
Volume channels
A channel is just a name a sound is emitted under. Each player has a volume per channel (1.0 when never set), and every recipient gets the sound's volume multiplied by their own. A player at 0 is not sent the sound at all.
// Emit on a channel
g_pSource2ModSounds->EmitSoundToAll("jb.duel", 1.0f, 1.0f, "music");
// A player's preference, e.g. from a !volume menu
g_pSource2ModSounds->SetPlayerVolume(player->GetPlayerSlot(), "music", 0.4f);
float v = g_pSource2ModSounds->GetPlayerVolume(player->GetPlayerSlot(), "music");
g_pSource2ModSounds->ResetPlayerVolumes(player->GetPlayerSlot());// Emit on a channel
Source2ModSounds.EmitSoundToAll("jb.duel", 1.0, 1.0, "music");
// A player's preference, e.g. from a !volume menu
Source2ModSounds.SetPlayerVolume(player.GetPlayerSlot(), "music", 0.4);
float v = Source2ModSounds.GetPlayerVolume(player.GetPlayerSlot(), "music");
Source2ModSounds.ResetPlayerVolumes(player.GetPlayerSlot());SetPlayerVolume() applies straight away to what the player is still hearing
on that channel — it gets louder, quieter, or stops — so a volume menu gives
instant feedback.
Channel volumes are forgotten when the player disconnects, and Source2Mod
stores nothing on disk. Load the player's preferences from wherever your
plugin keeps them (a database, an HTTP backend) when they connect, and call
SetPlayerVolume() for every channel — including ones whose default is not
1.0.
Hooking the game's sounds
HOOK_SOUND(handler) installs a SoundHook
(Source2ModCallback<Action(ISource2ModSound* sound)>) that sees every sound the game
is about to send. Return Action::Ignore to leave it alone,
Action::Override to send it with your changes, Action::Supersede to drop it.
A script calls Source2ModSounds.HookSound(handler) and returns
Action_Ignore, Action_Override or Action_Supersede.
HOOK_SOUND([](ISource2ModSound* sound) -> Action
{
// Sounds travel as hashes. Hash the names you care about once and
// compare numbers.
static const uint32_t s_nKnifeHit = g_pSource2ModSounds->HashSoundName("Weapon_Knife.Hit");
if (sound->GetNameHash() != s_nKnifeHit)
return Action::Ignore;
sound->SetPitch(1.5f);
return Action::Override;
});public void OnPluginStart()
{
Source2ModSounds.HookSound(OnSound);
}
public Source2ModAction OnSound(ISource2ModSound sound)
{
// Sounds travel as hashes. Hash the names you care about once and
// compare numbers.
static int s_KnifeHit;
if (!s_KnifeHit)
s_KnifeHit = Source2ModSounds.HashSoundName("Weapon_Knife.Hit");
if (sound.GetNameHash() != s_KnifeHit)
return Action_Ignore;
sound.SetPitch(1.5);
return Action_Override;
}- A hooked sound arrives as a hash.
GetName()only resolves names the Source2Mod has seen — anything you emitted or passed toHashSoundName()— and returns""otherwise.LookupSoundName(hash)returnsnullptrfor unknown hashes (falsein a script). - Do not call
Emit()inside the hook — the sound is already on its way; change it and returnAction::Override. - Sounds you send through this interface do not come back through the hook.
- Client-predicted sounds (a player's own shots, knife swings, footsteps) are played locally by that client and never reach the server; the copy the other players get does pass through the hook.
- A plugin may hook as often as it likes; each handler runs.
HOOK_SOUND()returns aSource2ModHookId;g_pSource2ModSounds->UnhookSound(id)removes that handler, andUNHOOK_SOUND(handler)removes a function or object-and-method handler by passing it again (a lambda only goes by id). A script unhooks by the idHookSound()returned:Source2ModSounds.UnhookSound(id).
Pitfalls
- Main thread only. Everything in this interface belongs on the main thread.
- Keep the guid, not the object. A guid is all you need to stop or change a
sound later; the
ISource2ModSoundcan be deleted right afterEmit(). A timer that stops a sound should capture the guid by value. - Unload. The sound hook and any
ISource2ModSoundobjects your plugin leaves behind are removed when it unloads. AddAllRecipients()is a snapshot. A player who connects later does not get a sound that was already emitted.
Reference
ISource2ModSoundsAPI reference- Scheduler — changing or stopping a sound after a delay