Skip to content

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.