Audio and effects
Lesson 9.5
Playing a sound is only the start. You can also change how every sound in the game sounds while it plays: muffle it behind a wall, add an echo in a cave, make a voice sound like a radio, or create sound from nothing. This lesson shows the tools. The full list is in the Audio and effects guide.
Everything here runs on the client, in client events like CL_EV_CLIENT_SOUND_PLAY and CL_EV_TICK_CLIENT_POST.
Every sound has a handle
CL_EV_CLIENT_SOUND_PLAY happens before a sound starts. handle in its payload is the sound itself:
#include <clight_easy.h>
static void on_sound(void* payload, void* user) {
cl_ev_sound_play* e = CL_EVENT(cl_ev_sound_play, payload);
(void)user;
if (e->category == CL_SOUND_NEUTRAL) cl_sound_set_volume(e->handle, 0.3f); /* quieter animals */
}
static cl_result mod_main(void) { return CL_ON(CL_EV_CLIENT_SOUND_PLAY, on_sound); }
CLIGHT_MOD_ENTRY(mod_main)
The cl_sound handle works as long as the sound plays (then calls return CL_E_STALE_HANDLE):
| Function | What it does |
|---|---|
cl_sound_set_volume(s, v), cl_sound_volume(s, &v) |
Its volume (0 to 1). |
cl_sound_set_pitch(s, p) |
Its pitch (0.5 to 2). |
cl_sound_set_position(s, pos), cl_sound_position(s, &pos) |
Moves it. |
cl_sound_attach(s, entity, offset) |
It follows the entity. |
cl_sound_set_range(s, blocks) |
How far away it can be heard. |
cl_sound_playing(out, cap, &n) |
Every sound playing right now. |
CL_EV_CLIENT_SOUND_STOP tells you when a sound ends.
Filters
A low-pass filter removes the high notes (a wall between you and the sound), a high-pass filter the low notes (a tiny radio speaker). Start from CL_AUDIO_FILTER_INIT and change what you need:
cl_audio_filter wall = CL_AUDIO_FILTER_INIT; /* a low-pass with every gain at 1 */
wall.gain = 0.6f;
wall.gain_hf = 0.15f;
cl_sound_set_filter(e->handle, &wall);
cl_audio_filter radio = CL_AUDIO_FILTER_INIT;
radio.type = CL_FILTER_HIGHPASS;
radio.gain_lf = 0.3f;
cl_sound_set_filter(e->handle, &radio);
cl_sound_set_filter(e->handle, NULL); /* normal again */
Changes are smoothed by the engine, so you can update a filter every tick without clicks.
Effect buses
An echo or reverb is one shared bus that many sounds feed:
static cl_audio_bus g_cave;
/* buses and routes can be made as soon as the mod loads: the game creates them when its sound starts */
static cl_result mod_main(void) {
cl_result r = cl_audio_bus_create(CL_STR("reverb"), &g_cave);
if (r != CL_OK) return r;
cl_audio_bus_set(g_cave, CL_STR("decay_time"), 3.0f);
cl_audio_bus_set(g_cave, CL_STR("gain"), 0.4f);
/* every world sound goes into the cave: blocks, mobs, players, ambience, weather, voice */
return cl_audio_route_default(g_cave, 0.5f, 0, true, NULL);
}
cl_audio_route_default sends every sound of the categories to the bus, including sounds already playing. 0 means every world category; pick some with CL_AUDIO_BLOCKS | CL_AUDIO_HOSTILE | .... One sound can also feed a bus by itself with cl_sound_set_send(s, bus, gain, NULL). cl_audio_max_sends() says how many buses one sound can feed. Other effects: "echo", "chorus", "flanger", "distortion", "ring_modulator", "pitch_shifter", "equalizer", and more (the reference lists their parameters).
The listener and the tracer
cl_audio_listener l;
if (cl_audio_get_listener(&l) == CL_OK && l.in_fluid == CL_FLUID_WATER) {
cl_audio_filter under = CL_AUDIO_FILTER_INIT;
under.gain = 0.8f;
under.gain_hf = 0.1f;
cl_audio_set_default_filter(&under); /* every positional sound */
}
cl_audio_trace_paths looks at the blocks on a straight line, natively and fast:
cl_audio_trace_opts o = CL_AUDIO_TRACE_OPTS_INIT; /* max_blocks = 48 */
cl_audio_trace t;
if (cl_audio_trace_paths(sound_pos, l.pos, &o, &t) == CL_OK && t.hit) {
cl_audio_filter f = CL_AUDIO_FILTER_INIT;
f.gain = t.gain;
f.gain_hf = t.gain_hf; /* ready-made values for that wall */
cl_sound_set_filter(s, &f);
}
t.stone, t.wood, … count the blocks of each material. cl_audio_trace_acoustics gives the whole room (reverb time, how closed it is) from a worker thread: call it every tick until ready is true.
Voice chat
Players talking in voice chat are sounds too:
static void on_voice(void* payload, void* user) {
cl_ev_voice_stream* e = CL_EVENT(cl_ev_voice_stream, payload);
(void)user;
if (e->kind == CL_VOICE_GROUP) {
cl_audio_filter radio = CL_AUDIO_FILTER_INIT;
radio.type = CL_FILTER_BANDPASS;
radio.gain_lf = 0.2f;
radio.gain_hf = 0.4f;
cl_sound_set_filter(e->handle, &radio); /* walkie-talkie */
}
}
/* in mod_main: CL_ON(CL_EV_VOICE_STREAM_OPEN, on_voice); */
Making your own sound
A stream plays 16-bit samples your mod computes:
static cl_audio_stream g_tone;
static double g_phase;
static void on_tick(void* payload, void* user) {
(void)payload;
(void)user;
if (!g_tone.h) { /* streams open once the game's sound runs (in a tick or an event) */
cl_audio_stream_opts o = CL_AUDIO_STREAM_OPTS_INIT; /* 48000 Hz, mono */
o.relative = true;
o.category = CL_SOUND_AMBIENT;
if (cl_audio_stream_open(&o, &g_tone) != CL_OK) return;
}
int32_t queued = 0;
cl_audio_stream_queued(g_tone, &queued);
if (queued >= 4800) return; /* 0.1 s ahead is enough */
int16_t buf[2400];
for (int i = 0; i < 2400; ++i) {
buf[i] = (int16_t)(sin(g_phase) * 3000);
g_phase += 2 * 3.14159265 * 440 / 48000;
}
cl_audio_stream_push(g_tone, (cl_str){(const char*)buf, sizeof buf});
}
static cl_result mod_main(void) { return CL_ON(CL_EV_TICK_CLIENT_POST, on_tick); }
(Include <math.h> for sin.) The samples are little-endian, which is what WebAssembly uses, so a plain int16_t array works.
Try it
Make every hostile mob sound echo: create an "echo" bus with a delay of 0.2 seconds, and send hostile sounds to it with a default route.
Show a solution
#include <clight_easy.h>
static cl_result mod_main(void) {
cl_audio_bus echo;
cl_result r = cl_audio_bus_create(CL_STR("echo"), &echo);
if (r != CL_OK) return r;
cl_audio_bus_set(echo, CL_STR("delay"), 0.2f);
cl_audio_bus_set(echo, CL_STR("feedback"), 0.3f);
return cl_audio_route_default(echo, 0.6f, CL_AUDIO_HOSTILE, true, NULL);
}
CLIGHT_MOD_ENTRY(mod_main)
A default route also catches the zombies that were already groaning when your mod started.
Recap: handle in CL_EV_CLIENT_SOUND_PLAY is the sound; cl_sound_set_filter colours it, buses (cl_audio_bus_create) add echo and reverb, cl_audio_route_default sends whole categories, cl_audio_trace_paths sees the walls, voice chat streams are sounds, and cl_audio_stream_open plays sound your mod makes.