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 ev::ClientSoundPlay and ev::TickClientPost.
Every sound is a clight::Sound
ev::ClientSoundPlay happens before a sound starts. e.handle is the sound itself; wrap it in clight::Sound to get its methods:
#include <clight.hpp>
clight::on<clight::ev::ClientSoundPlay>([](cl_ev_sound_play& e) {
clight::Sound s(e.handle);
if (e.category == CL_SOUND_NEUTRAL) s.set_volume(0.3f); // quieter animals
}).detach();
The methods return clight::Expected<...>, so you can check them or ignore them:
| Method | What it does |
|---|---|
set_volume(v), volume() |
Its volume (0 to 1). |
set_pitch(p), pitch() |
Its pitch (0.5 to 2). |
set_position(pos), position() |
Moves it. |
attach(entity, offset) |
It follows the entity. |
set_range(blocks), range() |
How far away it can be heard. |
get_category(), event() |
What it is. |
clight::sound::playing() lists every sound playing right now, and ev::ClientSoundStop tells you when one ends. After a sound ended, the methods return an error (CL_E_STALE_HANDLE) instead of doing anything.
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):
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;
s.set_filter(&wall);
cl_audio_filter radio = CL_AUDIO_FILTER_INIT;
radio.type = CL_FILTER_HIGHPASS;
radio.gain_lf = 0.3f;
s.set_filter(&radio);
s.set_filter(nullptr); // normal again
The engine smooths every change, so a filter that follows a moving mob never clicks.
Effect buses
An echo or reverb is one shared bus that many sounds feed:
class Mod final : public clight::Mod {
public:
clight::AudioBus cave;
void on_construct() override {
// buses and routes can be made as soon as the mod loads: the game creates them when its sound starts
if (auto b = clight::audio::bus_create("reverb")) {
cave = *b;
cave.set("decay_time", 3.0f);
cave.set("gain", 0.4f);
// every world sound goes into the cave (0 = blocks, mobs, players, ambience, weather, voice)
cave.route_default(0.5f, 0, true, nullptr);
}
}
};
CLIGHT_MOD(Mod)
route_default sends every sound of the categories to the bus, including sounds already playing; pick categories with CL_AUDIO_BLOCKS | CL_AUDIO_HOSTILE | .... A single sound can feed a bus with s.set_send(cave, 0.5f, nullptr). clight::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). Buses chain with echo.set_output(cave).
The listener and the tracer
if (auto l = clight::audio::get_listener(); l && l->in_fluid == CL_FLUID_WATER) {
cl_audio_filter under = CL_AUDIO_FILTER_INIT;
under.gain = 0.8f;
under.gain_hf = 0.1f;
clight::audio::set_default_filter(&under); // every positional sound
}
clight::audio::trace_paths(from, to, &opts) looks at the blocks on a straight line, natively and fast. Its gain and gain_hf are ready-made filter values for the wall it found:
cl_audio_trace_opts opts = CL_AUDIO_TRACE_OPTS_INIT; // max_blocks = 48
if (auto t = clight::audio::trace_paths(sound_pos, listener_pos, &opts); t && t->hit) {
cl_audio_filter f = CL_AUDIO_FILTER_INIT;
f.gain = t->gain;
f.gain_hf = t->gain_hf;
s.set_filter(&f);
}
clight::audio::trace_acoustics(from, to) gives the whole room (reverb time, how closed it is) from a worker thread: ask every tick until ready is true.
Voice chat
Players talking in voice chat are sounds too:
clight::on<clight::ev::VoiceStreamOpen>([](cl_ev_voice_stream& e) {
if (e.kind != CL_VOICE_GROUP) return;
cl_audio_filter radio = CL_AUDIO_FILTER_INIT;
radio.type = CL_FILTER_BANDPASS;
radio.gain_lf = 0.2f;
radio.gain_hf = 0.4f;
clight::Sound(e.handle).set_filter(&radio); // walkie-talkie
}).detach();
ev::VoiceTalking tells you who starts or stops talking.
Making your own sound
A stream plays 16-bit samples your mod computes:
#include <cmath>
#include <vector>
static clight::AudioStream g_tone;
static double g_phase = 0;
// in on_construct():
clight::on<clight::ev::TickClientPost>([](cl_ev_tick&) {
if (!g_tone) { // 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;
auto st = clight::audio::stream_open(&o);
if (!st) return;
g_tone = *st;
}
if (g_tone.queued().value_or(0) >= 4800) return; // 0.1 s ahead is enough
std::vector<int16_t> buf(2400);
for (auto& v : buf) {
v = static_cast<int16_t>(std::sin(g_phase) * 3000);
g_phase += 2 * 3.14159265 * 440 / 48000;
}
g_tone.push(std::string_view(reinterpret_cast<const char*>(buf.data()), buf.size() * 2));
}).detach();
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.hpp>
class Mod final : public clight::Mod {
public:
void on_construct() override {
auto echo = clight::audio::bus_create("echo");
if (!echo) return;
echo->set("delay", 0.2f);
echo->set("feedback", 0.3f);
echo->route_default(0.6f, CL_AUDIO_HOSTILE, true, nullptr);
}
};
CLIGHT_MOD(Mod)
A default route also catches the zombies that were already groaning when your mod started.
Recap: clight::Sound(e.handle) in ev::ClientSoundPlay is the sound; set_filter colours it, buses (clight::audio::bus_create) add echo and reverb, route_default sends whole categories, trace_paths sees the walls, voice chat streams are sounds, and clight::audio::stream_open plays sound your mod makes.