diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 99466ddab..9fdd8d0b3 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -26,10 +26,14 @@ jobs: brew install glfw3 glew libpng openal-soft luajit libvorbis skypjack/entt/entt googletest glm - name: Configure - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_TESTS=ON -DVOXELENGINE_BUILD_APPDIR=1 -DVC_BUILD_NAME="${{ inputs.build_name }}" + run: | + cmake -DOPENAL_INCLUDE_DIR=$(brew --prefix openal-soft)/include \ + -DOPENAL_LIBRARY=$(brew --prefix openal-soft)/lib/libopenal.dylib \ + -S . -B build -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_TESTS=ON -DVOXELENGINE_BUILD_APPDIR=1 -DVC_BUILD_NAME="${{ inputs.build_name }}" - name: Build - run: cmake --build build -t install + run: | + cmake --build build -t install - name: Make fix_dylibs.sh executable run: chmod +x dev/fix_dylibs.sh diff --git a/res/content/base/block_materials/carpet.json b/res/content/base/block_materials/carpet.json index 4d4d11dca..00c3688ec 100644 --- a/res/content/base/block_materials/carpet.json +++ b/res/content/base/block_materials/carpet.json @@ -1,5 +1,6 @@ { "steps-sound": "steps/carpet", "place-sound": "blocks/carpet_place", - "break-sound": "blocks/carpet_break" + "break-sound": "blocks/carpet_break", + "sound-absorption": 0.96 } diff --git a/res/content/base/block_materials/glass.json b/res/content/base/block_materials/glass.json index fd2e23238..8ad365675 100644 --- a/res/content/base/block_materials/glass.json +++ b/res/content/base/block_materials/glass.json @@ -1,5 +1,6 @@ { "steps-sound": "steps/glass", "place-sound": "blocks/glass_place", - "break-sound": "blocks/glass_break" + "break-sound": "blocks/glass_break", + "sound-absorption": 0.3 } diff --git a/res/content/base/block_materials/grass.json b/res/content/base/block_materials/grass.json index 148c80c09..ff091aa95 100644 --- a/res/content/base/block_materials/grass.json +++ b/res/content/base/block_materials/grass.json @@ -1,5 +1,6 @@ { "steps-sound": "steps/grass", "place-sound": "steps/grass", - "break-sound": "steps/grass" + "break-sound": "steps/grass", + "sound-absorption": 0.85 } diff --git a/res/content/base/block_materials/grass_block.json b/res/content/base/block_materials/grass_block.json index d45672a28..98ed4618a 100644 --- a/res/content/base/block_materials/grass_block.json +++ b/res/content/base/block_materials/grass_block.json @@ -2,5 +2,6 @@ "steps-sound": "steps/grass", "place-sound": "blocks/ground_place", "break-sound": "blocks/ground_break", - "hit-sound": "blocks/ground_hit" + "hit-sound": "blocks/ground_hit", + "sound-absorption": 0.8 } diff --git a/res/content/base/block_materials/ground.json b/res/content/base/block_materials/ground.json index 951cb5dfe..8e78a8563 100644 --- a/res/content/base/block_materials/ground.json +++ b/res/content/base/block_materials/ground.json @@ -2,5 +2,6 @@ "steps-sound": "steps/ground", "place-sound": "blocks/ground_place", "break-sound": "blocks/ground_break", - "hit-sound": "blocks/ground_hit" + "hit-sound": "blocks/ground_hit", + "sound-absorption": 0.7 } diff --git a/res/content/base/block_materials/metal.json b/res/content/base/block_materials/metal.json index 6f45903e1..0ab1b2b28 100644 --- a/res/content/base/block_materials/metal.json +++ b/res/content/base/block_materials/metal.json @@ -1,5 +1,6 @@ { "steps-sound": "steps/metal", "place-sound": "blocks/metal_place", - "break-sound": "blocks/metal_break" + "break-sound": "blocks/metal_break", + "sound-absorption": 0.1 } diff --git a/res/content/base/block_materials/sand.json b/res/content/base/block_materials/sand.json index 67316ba36..c047aab9c 100644 --- a/res/content/base/block_materials/sand.json +++ b/res/content/base/block_materials/sand.json @@ -1,5 +1,6 @@ { "steps-sound": "steps/sand", "place-sound": "blocks/ground_place", - "break-sound": "blocks/ground_break" + "break-sound": "blocks/ground_break", + "sound-absorption": 0.8 } diff --git a/res/content/base/block_materials/stone.json b/res/content/base/block_materials/stone.json index 43f579e7a..75052af52 100644 --- a/res/content/base/block_materials/stone.json +++ b/res/content/base/block_materials/stone.json @@ -2,5 +2,6 @@ "steps-sound": "steps/stone", "place-sound": "blocks/stone_place", "break-sound": "blocks/stone_break", - "hit-sound": "blocks/stone_hit" + "hit-sound": "blocks/stone_hit", + "sound-absorption": 0.3 } diff --git a/res/content/base/block_materials/wood.json b/res/content/base/block_materials/wood.json index 0c8e9a469..edf1801c2 100644 --- a/res/content/base/block_materials/wood.json +++ b/res/content/base/block_materials/wood.json @@ -2,5 +2,6 @@ "steps-sound": "steps/wood", "place-sound": "blocks/wood_place", "break-sound": "blocks/wood_break", - "hit-sound": "blocks/wood_hit" + "hit-sound": "blocks/wood_hit", + "sound-absorption": 0.5 } diff --git a/res/layouts/pages/settings_audio.xml.lua b/res/layouts/pages/settings_audio.xml.lua index c9b4be4b6..37609a900 100644 --- a/res/layouts/pages/settings_audio.xml.lua +++ b/res/layouts/pages/settings_audio.xml.lua @@ -24,6 +24,14 @@ function update_setting(x, id, name, postfix) ) end +function create_checkbox(id, name, tooltip) + tooltip = tooltip or '' + document.root:add(string.format( + "%s", + id, app.str_setting(id), gui.str(tooltip, "settings"), gui.str(name, "settings") + )) +end + local initialized = false function on_open() @@ -71,4 +79,6 @@ function on_open() if info then selectbox.value = info.device_specifier end + + create_checkbox("audio.acoustic-effects", "Acoustic effects", "audio.acoustic-effects.tooltip") end diff --git a/res/scripts/stdcmd.lua b/res/scripts/stdcmd.lua index 900371e49..779ad50bf 100644 --- a/res/scripts/stdcmd.lua +++ b/res/scripts/stdcmd.lua @@ -17,6 +17,7 @@ console.add_command( "clear", "Clears the console", function() + if not hud then error("command is unavailable in headless mode") return end local document = Document.new("core:console") document.log.text = "" end diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt index ae19130e3..a6c9c5391 100644 --- a/res/texts/en_US.txt +++ b/res/texts/en_US.txt @@ -17,10 +17,12 @@ devtools.traceback=Traceback (most recent call first) devtools.output=Output # Tooltips +audio.acoustic-effects.tooltip=Acoustic effects may not be supported by your system graphics.gamma.tooltip=Lighting brightness curve graphics.backlight.tooltip=Backlight to prevent total darkness graphics.dense-render.tooltip=Enables transparency in blocks like leaves graphics.soft-lighting.tooltip=Enables blocks soft lighting +graphics.advanced-render.tooltip=Use graphics pipeline supporting advanced effects like shadows, SSAO # settings settings.Controls Search Mode=Search by attached button name diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index fd35f10ef..66dc40a42 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -38,10 +38,12 @@ error.dependency-version-not-met=Версия зависимости не соо pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)? # Подсказки +audio.acoustic-effects.tooltip=Акустические эффекты могут не поддерживаться системой graphics.gamma.tooltip=Кривая яркости освещения graphics.backlight.tooltip=Подсветка, предотвращающая полную темноту graphics.dense-render.tooltip=Включает прозрачность блоков, таких как листья graphics.soft-lighting.tooltip=Включает мягкое освещение у блоков +graphics.advanced-render.tooltip=Использовать графический конвейер, поддерживающий продвинутые эффекты, такие как тени и SSAO # Меню menu.Apply=Применить @@ -111,6 +113,7 @@ settings.Borderless=Безрамочный settings.Microphone=Микрофон settings.microphone.None=Нет settings.Clouds quality=Качество облаков +settings.Acoustic effects=Акустические эффекты # Управление chunks.reload=Перезагрузить Чанки diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index 41ce83c08..e7494d306 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -40,6 +40,22 @@ static bool check_alc_errors(ALCdevice* device, const char* context) { return true; } +static LPALAUXILIARYEFFECTSLOTI alAuxiliaryEffectSloti = nullptr; +static LPALGENAUXILIARYEFFECTSLOTS alGenAuxiliaryEffectSlots = nullptr; +static LPALGENEFFECTS alGenEffects = nullptr; +static LPALDELETEEFFECTS alDeleteEffects = nullptr; +static LPALISEFFECT alIsEffect = nullptr; +static LPALEFFECTI alEffecti = nullptr; +static LPALEFFECTF alEffectf = nullptr; +static LPALGENFILTERS alGenFilters = nullptr; +static LPALDELETEFILTERS alDeleteFilters = nullptr; +static LPALISFILTER alIsFilter = nullptr; +static LPALFILTERI alFilteri = nullptr; +static LPALFILTERF alFilterf = nullptr; + +static inline constexpr int REVERB_EFFECT = 0; +static inline constexpr int LOWPASS_FILTER = 0; + ALSound::ALSound( ALAudio* al, uint buffer, const std::shared_ptr& pcm, bool keepPCM ) @@ -376,12 +392,19 @@ void ALSpeaker::setLoop(bool loop) { void ALSpeaker::play() { paused = false; manuallyStopped = false; - auto p_channel = get_channel(this->channel); + auto channel = get_channel(this->channel); AL_CHECK(alSourcef( source, AL_GAIN, - volume * p_channel->getVolume() + volume * channel->getVolume() )); + if (al->useEffects && channel->isEffectsApplied()) { + AL_CHECK(alSource3i(source, AL_AUXILIARY_SEND_FILTER, al->effectSlots[0], 0, al->filters[0])); + //AL_CHECK(alSourcei(source, AL_DIRECT_FILTER, al->filters[LOWPASS_FILTER])); // TODO: use lowpass filter + } else { + AL_CHECK(alSource3i(source, AL_AUXILIARY_SEND_FILTER, 0, 0, 0)); + AL_CHECK(alSourcei(source, AL_DIRECT_FILTER, 0)); + } AL_CHECK(alSourcePlay(source)); } @@ -455,15 +478,22 @@ int ALSpeaker::getPriority() const { return priority; } - bool ALSpeaker::isManuallyStopped() const { return manuallyStopped; } static bool alc_enumeration_ext = false; -ALAudio::ALAudio(ALCdevice* device, ALCcontext* context) - : device(device), context(context) { +ALAudio::ALAudio( + ALCdevice* device, + ALCcontext* context, + bool useEffects, + const AudioSettings& settings +) + : device(device), + context(context), + settings(settings), + useEffects(useEffects) { ALCint size; alcGetIntegerv(device, ALC_ATTRIBUTES_SIZE, 1, &size); std::vector attrs(size); @@ -487,6 +517,11 @@ ALAudio::ALAudio(ALCdevice* device, ALCcontext* context) } alDopplerFactor(1.0 / 3.0); + alGetError(); + + if (useEffects) { + this->useEffects = initEffects(); + } } ALAudio::~ALAudio() { @@ -513,6 +548,80 @@ ALAudio::~ALAudio() { context = nullptr; } +template +static bool get_proc_address(const char* name, T& ptr) { + ptr = (T) alGetProcAddress(name); + return ptr != nullptr; +} + +static bool request_efx_ext_functions() { + return get_proc_address("alAuxiliaryEffectSloti", alAuxiliaryEffectSloti) + && get_proc_address("alGenAuxiliaryEffectSlots", alGenAuxiliaryEffectSlots) + && get_proc_address("alGenEffects", alGenEffects) + && get_proc_address("alDeleteEffects", alDeleteEffects) + && get_proc_address("alIsEffect", alIsEffect) + && get_proc_address("alEffecti", alEffecti) + && get_proc_address("alEffectf", alEffectf) + && get_proc_address("alGenFilters", alGenFilters) + && get_proc_address("alDeleteFilters", alDeleteFilters) + && get_proc_address("alIsFilter", alIsFilter) + && get_proc_address("alFilteri", alFilteri) + && get_proc_address("alFilterf", alFilterf); +} + +bool ALAudio::initEffects() { + if (!request_efx_ext_functions()) { + logger.error() << "could not get effects extension function pointers"; + return false; + } + for (uint i = 0; i < maxEffectSlots; i++) { + effectSlots.emplace_back(); + alGenAuxiliaryEffectSlots(1, &effectSlots[i]); + if (alGetError() != AL_NO_ERROR) { + break; + } + } + logger.info() << "created " << effectSlots.size() << " effect slots"; + + for (uint i = 0; i < 4; i++) { + effects.emplace_back(); + alGenEffects(1, &effects[i]); + if (alGetError() != AL_NO_ERROR || !alIsEffect(effects[i])) { + logger.error() << "could not to create effect #" << i; + return false; + } + } + logger.info() << "created " << effects.size() << " effects"; + + for (uint i = 0; i < filters.size(); i++) { + alGenFilters(1, &filters[i]); + if (alGetError() != AL_NO_ERROR || !alIsFilter(filters[i])) { + logger.error() << "could not to create filter #" << i; + return false; + } + } + + alEffecti(effects[REVERB_EFFECT], AL_EFFECT_TYPE, AL_EFFECT_REVERB); + if (alGetError() != AL_NO_ERROR) { + logger.error() << "reverb effect is not supported"; + return false; + } + + alFilteri(filters[LOWPASS_FILTER], AL_FILTER_TYPE, AL_FILTER_LOWPASS); + if (alGetError() != AL_NO_ERROR) { + logger.error() << "lowpass filter is not supported"; + return false; + } + alFilterf(filters[LOWPASS_FILTER], AL_LOWPASS_GAIN, 1.0f); + alFilterf(filters[LOWPASS_FILTER], AL_LOWPASS_GAINHF, 0.01f); + + alAuxiliaryEffectSloti(effectSlots[0], AL_EFFECTSLOT_EFFECT, effects[0]); + if (alGetError() == AL_NO_ERROR) { + logger.info() << "successfully loaded effect into effect slot"; + } + return true; +} + std::unique_ptr ALAudio::createSound( std::shared_ptr pcm, bool keepPCM ) { @@ -592,19 +701,36 @@ std::unique_ptr ALAudio::openInputDevice( ); } -std::unique_ptr ALAudio::create() { +std::unique_ptr ALAudio::create(const AudioSettings& settings) { alc_enumeration_ext = alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT"); ALCdevice* device = alcOpenDevice(nullptr); - if (device == nullptr) return nullptr; + if (device == nullptr) { + return nullptr; + } + bool effects = true; + ALint attribs[4] {}; + if (alcIsExtensionPresent(device, "ALC_EXT_EFX") == AL_TRUE) { + logger.info() << "AL effects extension present"; + attribs[0] = ALC_MAX_AUXILIARY_SENDS; + attribs[1] = 4; + } else { + logger.warning() << "AL effects extension is not supported"; + effects = false; + } ALCcontext* context = alcCreateContext(device, nullptr); if (!alcMakeContextCurrent(context)) { alcCloseDevice(device); return nullptr; } AL_CHECK(); + + ALCint sends = 0; + alcGetIntegerv(device, ALC_MAX_AUXILIARY_SENDS, 1, &sends); + logger.info() << "device supports " << sends << " aux sends per source"; + logger.info() << "initialized"; - return std::make_unique(device, context); + return std::make_unique(device, context, effects, settings); } uint ALAudio::getFreeSource() { @@ -664,3 +790,84 @@ void ALAudio::setListener( void ALAudio::update(double) { } + +void ALAudio::setAcoustics(Acoustics acoustics) { + if (!useEffects) { + return; + } + if (!settings.acousticEffects.get()) { + alAuxiliaryEffectSloti(effectSlots[0], AL_EFFECTSLOT_EFFECT, 0); + return; + } + int reverbEffect = effects[REVERB_EFFECT]; + AL_CHECK(alEffectf( + reverbEffect, + AL_REVERB_GAIN, + glm::max(0.1f, glm::min(acoustics.reverbGain, 0.8f) * + (1.0f - glm::min(1.0f, acoustics.reverbAbsorption))) + )); + AL_CHECK(alEffectf( + reverbEffect, + AL_REVERB_REFLECTIONS_DELAY, + glm::clamp( + acoustics.reverbReflectionsDelay, + AL_REVERB_MIN_REFLECTIONS_DELAY, + AL_REVERB_MAX_REFLECTIONS_DELAY + ) + )); + AL_CHECK(alEffectf( + reverbEffect, + AL_REVERB_LATE_REVERB_DELAY, + glm::clamp( + acoustics.reverbLateReflectionsDelay, + AL_REVERB_MIN_LATE_REVERB_DELAY, + AL_REVERB_MAX_LATE_REVERB_DELAY + ) + )); + AL_CHECK(alEffectf( + reverbEffect, + AL_REVERB_DECAY_TIME, + glm::clamp( + acoustics.reverbDecayTime, + AL_REVERB_MIN_DECAY_TIME, + AL_REVERB_MAX_DECAY_TIME + ) + )); + AL_CHECK(alEffectf( + reverbEffect, + AL_REVERB_ROOM_ROLLOFF_FACTOR, + glm::clamp( + acoustics.reverbRoomRolloff, + AL_REVERB_MIN_ROOM_ROLLOFF_FACTOR, + AL_REVERB_MAX_ROOM_ROLLOFF_FACTOR + ) + )); + AL_CHECK(alEffectf( + reverbEffect, + AL_REVERB_REFLECTIONS_GAIN, + glm::clamp( + glm::pow(acoustics.reverbGain, 2.0f), + AL_REVERB_MIN_REFLECTIONS_GAIN, + AL_REVERB_MAX_REFLECTIONS_GAIN + ) + )); + AL_CHECK(alEffectf( + reverbEffect, + AL_REVERB_LATE_REVERB_GAIN, + glm::clamp( + glm::pow(acoustics.reverbGain, 2.0f), + AL_REVERB_MIN_LATE_REVERB_GAIN, + AL_REVERB_MAX_LATE_REVERB_GAIN + ) + )); + AL_CHECK(alEffectf( + reverbEffect, + AL_REVERB_DECAY_HFRATIO, + glm::clamp( + 1.0f - acoustics.reverbAbsorption, + AL_REVERB_MIN_DECAY_HFRATIO, + AL_REVERB_MAX_DECAY_HFRATIO + ) + )); + alAuxiliaryEffectSloti(effectSlots[0], AL_EFFECTSLOT_EFFECT, reverbEffect); +} diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp index 5642f222b..b405d1473 100644 --- a/src/audio/AL/ALAudio.hpp +++ b/src/audio/AL/ALAudio.hpp @@ -3,19 +3,19 @@ #include #include #include +#include #include #include #include "typedefs.hpp" #include "audio/audio.hpp" +#include "audio/effects.hpp" -#ifdef __APPLE__ -#include -#include -#else #include #include -#endif +#include + +struct AudioSettings; namespace audio { struct ALBuffer; @@ -176,10 +176,25 @@ namespace audio { std::vector allbuffers; std::vector freebuffers; - uint maxSources = 256; + uint maxEffectSlots = 64; + + const AudioSettings& settings; + + bool initEffects(); public: - ALAudio(ALCdevice* device, ALCcontext* context); + std::vector effectSlots; + std::vector effects; + std::array filters; + + bool useEffects; + + ALAudio( + ALCdevice* device, + ALCcontext* context, + bool effects, + const AudioSettings& settings + ); ~ALAudio(); uint getFreeSource(); @@ -214,10 +229,12 @@ namespace audio { void update(double delta) override; + void setAcoustics(Acoustics acoustics) override; + bool isDummy() const override { return false; } - static std::unique_ptr create(); + static std::unique_ptr create(const AudioSettings& settings); }; } diff --git a/src/audio/AL/alutil.hpp b/src/audio/AL/alutil.hpp index bfa701184..56e5b0f7e 100644 --- a/src/audio/AL/alutil.hpp +++ b/src/audio/AL/alutil.hpp @@ -1,17 +1,12 @@ #pragma once +#include "typedefs.hpp" + #include #include #include -#include "typedefs.hpp" - -#ifdef __APPLE__ -#include -#else #include -#endif - #include #define AL_CHECK(STATEMENT) \ diff --git a/src/audio/NoAudio.hpp b/src/audio/NoAudio.hpp index 0da1b4d63..c97794f36 100644 --- a/src/audio/NoAudio.hpp +++ b/src/audio/NoAudio.hpp @@ -104,6 +104,9 @@ namespace audio { void update(double delta) override { } + void setAcoustics(Acoustics) override { + } + bool isDummy() const override { return true; } diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index dc0e57463..e8fd96e14 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -27,7 +27,8 @@ namespace { static bool input_enabled = false; } -Channel::Channel(std::string name) : name(std::move(name)) { +Channel::Channel(std::string name, bool effects) + : name(std::move(name)), effects(effects) { } float Channel::getVolume() const { @@ -56,6 +57,10 @@ bool Channel::isPaused() const { return paused; } +bool Channel::isEffectsApplied() const { + return effects; +} + size_t PCMStream::readFully(char* buffer, size_t bufferSize, bool loop) { if (!isOpen()) { return 0; @@ -159,7 +164,7 @@ void audio::initialize( enabled = enabled && settings.enabled.get(); if (enabled) { logger.info() << "initializing ALAudio backend"; - backend = ALAudio::create().release(); + backend = ALAudio::create(settings).release(); } if (backend == nullptr) { if (enabled) { @@ -171,15 +176,16 @@ void audio::initialize( struct { std::string name; NumberSetting* setting; + bool effects; } builtin_channels[] { - {"master", &settings.volumeMaster}, - {"regular", &settings.volumeRegular}, - {"music", &settings.volumeMusic}, - {"ambient", &settings.volumeAmbient}, - {"ui", &settings.volumeUI} + {"master", &settings.volumeMaster, false}, + {"regular", &settings.volumeRegular, true}, + {"music", &settings.volumeMusic, false}, + {"ambient", &settings.volumeAmbient, true}, + {"ui", &settings.volumeUI, false} }; for (auto& channel : builtin_channels) { - create_channel(channel.name); + create_channel(channel.name, channel.effects); objects_keeper.keepAlive(channel.setting->observe([=](auto value) { audio::get_channel(channel.name)->setVolume(value * value); }, true)); @@ -419,12 +425,12 @@ Speaker* audio::get_speaker(speakerid_t id) { return found->second.get(); } -int audio::create_channel(const std::string& name) { +int audio::create_channel(const std::string& name, bool effects) { int index = get_channel_index(name); if (index != -1) { return index; } - channels.emplace_back(std::make_unique(name)); + channels.emplace_back(std::make_unique(name, effects)); return channels.size() - 1; } @@ -496,6 +502,10 @@ void audio::update(double delta) { } } +void audio::set_acoustics(Acoustics acoustics) { + backend->setAcoustics(std::move(acoustics)); +} + void audio::reset_channel(int index) { auto channel = get_channel(index); if (channel == nullptr) { diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index 642b9235d..39304fd41 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -7,6 +7,7 @@ #include "typedefs.hpp" #include "settings.hpp" #include "io/fwd.hpp" +#include "effects.hpp" namespace audio { /// @brief playing speaker uid @@ -42,8 +43,9 @@ namespace audio { /// @brief Channel volume setting float volume = 1.0f; bool paused = false; + bool effects; public: - Channel(std::string name); + Channel(std::string name, bool effects); /// @brief Get channel volume float getVolume() const; @@ -72,6 +74,9 @@ namespace audio { /// @brief Check if the channel is paused bool isPaused() const; + + /// @brief Check if the channel uses common acoustic effects / filters + bool isEffectsApplied() const; }; /// @brief Pulse-code modulation data @@ -394,6 +399,7 @@ namespace audio { virtual std::vector getInputDeviceNames() = 0; virtual std::vector getOutputDeviceNames() = 0; virtual void update(double delta) = 0; + virtual void setAcoustics(Acoustics acoustics) = 0; /// @brief Check if backend is an abstraction that does not internally /// work with actual audio data or play anything @@ -547,8 +553,9 @@ namespace audio { /// @brief Create new channel. /// All non-builtin channels will be destroyed on audio::reset() call /// @param name channel name + /// @param effects use common acoustic effects / filters /// @return new channel index - int create_channel(const std::string& name); + int create_channel(const std::string& name, bool effects); /// @brief Get channel index by name /// @param name channel name @@ -580,6 +587,8 @@ namespace audio { /// @param delta time elapsed since the last update (seconds) void update(double delta); + void set_acoustics(Acoustics acoustics); + /// @brief Stop all playing audio in channel, reset channel state void reset_channel(int channel); diff --git a/src/audio/effects.hpp b/src/audio/effects.hpp new file mode 100644 index 000000000..f8f10e4af --- /dev/null +++ b/src/audio/effects.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +namespace audio { + struct Effect { + struct Property { + using Value = std::variant; + int id; + Value def; + Value min; + Value max; + }; + std::map properties; + }; + + class EffectControl { + public: + EffectControl(const Effect* effect) : effect(effect) {}; + virtual ~EffectControl() = default; + + virtual void set(const std::string& name, Effect::Property::Value value) = 0; + protected: + const Effect* effect = nullptr; + }; + + struct Acoustics { + float reverbGain; + float reverbReflectionsDelay; + float reverbLateReflectionsDelay; + float reverbDecayTime; + float reverbRoomRolloff; + float reverbAbsorption; + }; +} diff --git a/src/devtools/stdin_cmd_reader.cpp b/src/devtools/stdin_cmd_reader.cpp new file mode 100644 index 000000000..8e4a48c22 --- /dev/null +++ b/src/devtools/stdin_cmd_reader.cpp @@ -0,0 +1,40 @@ +#include "stdin_cmd_reader.hpp" + +#include "engine/Engine.hpp" +#include "logic/CommandsInterpreter.hpp" +#include "coders/json.hpp" +#include "debug/Logger.hpp" + +#include +#include + +static debug::Logger logger("stdin-cmd"); + +static std::thread reader_thread; + +void cmd::start_stdin_cmd_reader(Engine& engine) { + reader_thread = std::thread([&engine]() { + auto& interpreter = engine.getCmd(); + logger.info() << "reader thread started"; + + std::string line; + while (std::getline(std::cin, line)) { + if (line.empty()) { + continue; + } + engine.postRunnable([line, &interpreter] () { + try { + auto result = interpreter.execute(line); + if (result.isString()) { + logger.info() << result.asString(); + } else { + logger.info() << json::stringify(result, true); + } + } catch (const std::exception& err) { + logger.error() << err.what(); + } + }); + } + }); + reader_thread.detach(); +} diff --git a/src/devtools/stdin_cmd_reader.hpp b/src/devtools/stdin_cmd_reader.hpp new file mode 100644 index 000000000..992054250 --- /dev/null +++ b/src/devtools/stdin_cmd_reader.hpp @@ -0,0 +1,7 @@ +#pragma once + +class Engine; + +namespace cmd { + void start_stdin_cmd_reader(Engine& engine); +} diff --git a/src/engine/CoreParameters.hpp b/src/engine/CoreParameters.hpp index c937bf3ad..b482f2bb5 100644 --- a/src/engine/CoreParameters.hpp +++ b/src/engine/CoreParameters.hpp @@ -6,6 +6,7 @@ struct CoreParameters { bool headless = false; bool testMode = false; + bool stdinCommands = false; std::filesystem::path resFolder = "res"; std::filesystem::path userFolder = "."; std::filesystem::path scriptFile; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 4799e9d77..e793224a9 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -15,6 +15,7 @@ #include "devtools/DebuggingServer.hpp" #include "devtools/Editor.hpp" #include "devtools/Project.hpp" +#include "devtools/stdin_cmd_reader.hpp" #include "EnginePaths.hpp" #include "frontend/locale.hpp" #include "frontend/menu.hpp" @@ -191,6 +192,9 @@ void Engine::initialize(CoreParameters coreParameters) { if (!params.headless) { project->loadProjectClientScript(); } + if (params.stdinCommands) { + cmd::start_stdin_cmd_reader(*this); + } } void Engine::loadSettings() { @@ -419,10 +423,20 @@ EngineSettings& Engine::getSettings() { } Assets* Engine::getAssets() { - return assets->getStorage(); + return assets ? assets->getStorage() : nullptr; +} + +Assets& Engine::requireAssets() { + if (isHeadless()) { + throw std::runtime_error("assets are not available in headless mode"); + } + return *assets->getStorage(); } AssetsLoader& Engine::acquireBackgroundLoader() { + if (isHeadless()) { + throw std::runtime_error("assets are not available in headless mode"); + } return assets->acquireBackgroundLoader(); } diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 4f4e68280..4f726b700 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -110,6 +110,7 @@ public: /// @brief Get active assets storage instance Assets* getAssets(); + Assets& requireAssets(); AssetsLoader& acquireBackgroundLoader(); diff --git a/src/graphics/render/Decorator.cpp b/src/graphics/render/Decorator.cpp index 420f6d0dc..b1957c697 100644 --- a/src/graphics/render/Decorator.cpp +++ b/src/graphics/render/Decorator.cpp @@ -23,9 +23,12 @@ #include "io/io.hpp" #include "audio/audio.hpp" #include "maths/util.hpp" +#include "debug/Logger.hpp" namespace fs = std::filesystem; +static debug::Logger logger("decorator"); + /// @brief Not greater than 64 for this BIG_PRIME value inline constexpr int UPDATE_AREA_DIAMETER = 32; /// @brief Number of blocks in the volume @@ -70,19 +73,90 @@ Decorator::Decorator( void Decorator::addParticles(const Block& def, const glm::ivec3& pos) { const auto& found = blockEmitters.find(pos); - if (found == blockEmitters.end()) { - auto treg = util::get_texture_region( - assets, def.particles->texture, "" - ); - blockEmitters[pos] = renderer.particles->add(std::make_unique( - level, - glm::vec3{pos.x + 0.5, pos.y + 0.5, pos.z + 0.5}, - *def.particles, - treg.texture, - treg.region, - -1 - )); + if (found != blockEmitters.end()) { + return; } + auto treg = util::get_texture_region( + assets, def.particles->texture, "" + ); + blockEmitters[pos] = renderer.particles->add(std::make_unique( + level, + glm::vec3{pos.x + 0.5, pos.y + 0.5, pos.z + 0.5}, + *def.particles, + treg.texture, + treg.region, + -1 + )); +} + +void Decorator::updateAcoustics(const Camera& camera) { + audio::Acoustics acoustics {}; + util::PseudoRandom random(34621U); + + auto& chunks = *player.chunks; + const auto& start = camera.position; + float rayLength = 40.0f; + + auto& blocks = level.content.getIndices()->blocks; + + int rays = 100; + int hit = 0; + float averageDistance = 0.0f; + float averageAbsorption = 0.0f; + float speedOfSound = 300.0f; + float minDistance = rayLength; + for (int i = 0; i < rays; i++) { + float u1 = random.randFloat(); + float u2 = random.randFloat(); + float z = 2.0f * u1 - 1.0f; + float phi = 2.0f * glm::pi() * u2; + float r = std::sqrt(1.0f - z * z); + + glm::vec3 dir = { r * std::cos(phi), r * std::sin(phi), z }; + + dir = glm::normalize(dir); + glm::vec3 end; + glm::ivec3 norm; + glm::ivec3 iend; + auto vox = chunks.rayCast(start, dir, rayLength, end, norm, iend); + if (vox == nullptr) { + continue; + } + auto distance = glm::distance(start, end); + if (distance >= rayLength * 0.98f) { + continue; + } + if (distance < minDistance) { + minDistance = distance; + } + auto def = blocks.get(vox->id); + if (def == nullptr) { + continue; + } + auto material = level.content.findBlockMaterial(def->material); + if (material == nullptr) { + return; + } + hit++; + averageDistance += distance; + averageAbsorption += material->soundAbsorption; + } + if (hit > 0) { + averageDistance /= hit; + averageAbsorption /= hit; + } + + float decayTime = 20.0f * (averageDistance / rayLength); + float escapeRatio = static_cast(rays - hit) / static_cast(rays); + + acoustics.reverbGain = glm::pow(hit / static_cast(rays), 3.0f); + acoustics.reverbReflectionsDelay = minDistance / speedOfSound; + acoustics.reverbLateReflectionsDelay = averageDistance / speedOfSound; + acoustics.reverbDecayTime = decayTime; + acoustics.reverbRoomRolloff = escapeRatio * 1.5f; + acoustics.reverbAbsorption = averageAbsorption; + + audio::set_acoustics(std::move(acoustics)); } void Decorator::updateRandom( @@ -283,4 +357,5 @@ void Decorator::update( } updateBlockEmitters(camera); updateTextNotes(); + updateAcoustics(camera); } diff --git a/src/graphics/render/Decorator.hpp b/src/graphics/render/Decorator.hpp index 181fdcb5d..4fe52c50a 100644 --- a/src/graphics/render/Decorator.hpp +++ b/src/graphics/render/Decorator.hpp @@ -48,6 +48,7 @@ class Decorator { void updateBlockEmitters(const Camera& camera); void updateTextNotes(); void addParticles(const Block& def, const glm::ivec3& pos); + void updateAcoustics(const Camera& camera); public: Decorator( Engine& engine, diff --git a/src/io/settings_io.cpp b/src/io/settings_io.cpp index 27e08c93f..af4547a14 100644 --- a/src/io/settings_io.cpp +++ b/src/io/settings_io.cpp @@ -44,6 +44,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) { builder.add("volume-ambient", &settings.audio.volumeAmbient); builder.add("volume-music", &settings.audio.volumeMusic); builder.add("input-device", &settings.audio.inputDevice); + builder.add("acoustic-effects", &settings.audio.acousticEffects); builder.addSection("display"); builder.add("width", &settings.display.width); diff --git a/src/logic/scripting/lua/libs/libassets.cpp b/src/logic/scripting/lua/libs/libassets.cpp index 0921f1b9c..82135ad86 100644 --- a/src/logic/scripting/lua/libs/libassets.cpp +++ b/src/logic/scripting/lua/libs/libassets.cpp @@ -15,10 +15,10 @@ using namespace scripting; static void load_texture( - const ubyte* bytes, size_t size, const std::string& destname + Assets& assets, const ubyte* bytes, size_t size, const std::string& destname ) { try { - engine->getAssets()->store(png::load_texture(bytes, size), destname); + assets.store(png::load_texture(bytes, size), destname); } catch (const std::runtime_error& err) { debug::Logger logger("lua.assetslib"); logger.error() << err.what(); @@ -34,6 +34,8 @@ static int l_request_texture(lua::State* L) { } static int l_load_texture(lua::State* L) { + auto& assets = engine->requireAssets(); + if (lua::isstring(L, 3) && lua::require_lstring(L, 3) != "png") { throw std::runtime_error("unsupportd image format"); } @@ -47,10 +49,13 @@ static int l_load_texture(lua::State* L) { lua::pop(L); } lua::pop(L); - load_texture(buffer.data(), buffer.size(), lua::require_string(L, 2)); + load_texture( + assets, buffer.data(), buffer.size(), lua::require_string(L, 2) + ); } else { auto string = lua::bytearray_as_string(L, 1); load_texture( + assets, reinterpret_cast(string.data()), string.size(), lua::require_string(L, 2) @@ -61,13 +66,15 @@ static int l_load_texture(lua::State* L) { } static int l_parse_model(lua::State* L) { + auto& assets = engine->requireAssets(); + auto format = lua::require_lstring(L, 1); auto string = lua::require_lstring(L, 2); auto name = lua::require_string(L, 3); if (format == "xml" || format == "vcm") { auto vcmModel = vcm::parse(name, string, format == "xml"); - engine->getAssets()->store( + assets.store( std::make_unique(std::move(vcmModel.squash())), name ); } else { @@ -79,7 +86,7 @@ static int l_parse_model(lua::State* L) { } static int l_to_canvas(lua::State* L) { - auto& assets = *engine->getAssets(); + auto& assets = engine->requireAssets(); auto alias = lua::require_lstring(L, 1); size_t sep = alias.rfind(':'); diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 60f894bda..382113171 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -33,7 +33,7 @@ using namespace scripting; static DocumentNode get_document_node_impl( lua::State*, const std::string& name, const std::string& nodeName, bool throwable=true ) { - auto doc = engine->getAssets()->get(name); + auto doc = engine->requireAssets().get(name); if (doc == nullptr) { if (throwable) { throw std::runtime_error("document '" + name + "' not found"); @@ -1027,7 +1027,7 @@ static int l_gui_str(lua::State* L) { static int l_gui_reindex(lua::State* L) { auto name = lua::require_string(L, 1); - auto doc = engine->getAssets()->get(name); + auto doc = engine->requireAssets().get(name); if (doc == nullptr) { throw std::runtime_error( "document '" + std::string(name) + "' not found" @@ -1125,7 +1125,7 @@ static int l_gui_load_document(lua::State* L) { engine->getGUI(), std::move(env), alias, filename, filename.string() ); auto document = documentPtr.get(); - engine->getAssets()->store(std::move(documentPtr), alias); + engine->requireAssets().store(std::move(documentPtr), alias); // namespace extension if (lua::istable(L, 4)) { diff --git a/src/settings.hpp b/src/settings.hpp index 171db217e..331ff3ee8 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -18,6 +18,8 @@ struct AudioSettings { NumberSetting volumeMusic {1.0f, 0.0f, 1.0f, setting_format::percent}; StringSetting inputDevice {"auto"}; + + FlagSetting acousticEffects {true}; }; struct DisplaySettings { diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 7cb3614ef..e41a0ea57 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -62,6 +62,10 @@ static bool perform_keyword( params.headless = true; return true; }, "", "run in headless mode."), + ArgC("--stdin-cmd", [¶ms]() -> bool { + params.stdinCommands = true; + return true; + }, "", "run commands from stdin."), ArgC("--tps", [¶ms, &reader]() -> bool { params.tps = reader.nextInt(); return true; diff --git a/src/voxels/Block.cpp b/src/voxels/Block.cpp index 65830efc5..11933a697 100644 --- a/src/voxels/Block.cpp +++ b/src/voxels/Block.cpp @@ -24,7 +24,8 @@ dv::value BlockMaterial::serialize() const { {"steps-sound", stepsSound}, {"place-sound", placeSound}, {"break-sound", breakSound}, - {"hit-sound", hitSound} + {"hit-sound", hitSound}, + {"sound-absorption", soundAbsorption} }); } @@ -34,6 +35,7 @@ void BlockMaterial::deserialize(const dv::value& src) { src.at("place-sound").get(placeSound); src.at("break-sound").get(breakSound); src.at("hit-sound").get(hitSound); + src.at("sound-absorption").get(soundAbsorption); } CoordSystem::CoordSystem(glm::ivec3 axisX, glm::ivec3 axisY, glm::ivec3 axisZ) diff --git a/src/voxels/Block.hpp b/src/voxels/Block.hpp index 579cf5982..e1a68c28f 100644 --- a/src/voxels/Block.hpp +++ b/src/voxels/Block.hpp @@ -147,6 +147,7 @@ struct BlockMaterial : Serializable { std::string placeSound; std::string breakSound; std::string hitSound; + float soundAbsorption = 0.5f; dv::value toTable() const; // for compatibility dv::value serialize() const override;