diff options
author | Nemrav <50055236+Nemrav@users.noreply.github.com> | 2024-08-06 01:48:36 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-06 01:48:36 +0200 |
commit | 82b16bcca7c74607a8885b882ec36f5202e7ef70 (patch) | |
tree | 0a9bd4f52c01315c3b38ce641a78c33bd8562be2 /extension | |
parent | fde15e554dc9ed458a838683c69d10262764db12 (diff) | |
parent | 9506f4160f0bd351f0853e6e8263ea927d9ec771 (diff) |
Merge pull request #243 from Nemrav/music
Music and sound effects
Diffstat (limited to 'extension')
6 files changed, 521 insertions, 5 deletions
diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation -Subproject 1f42a6255226b79d271df5060a8391f4ea00fc0 +Subproject adc7eb8ad07170ba8da18f684321a92d01447c2 diff --git a/extension/src/openvic-extension/register_types.cpp b/extension/src/openvic-extension/register_types.cpp index b2b6731..0b9d779 100644 --- a/extension/src/openvic-extension/register_types.cpp +++ b/extension/src/openvic-extension/register_types.cpp @@ -17,6 +17,7 @@ #include "openvic-extension/singletons/LoadLocalisation.hpp" #include "openvic-extension/singletons/MenuSingleton.hpp" #include "openvic-extension/singletons/ModelSingleton.hpp" +#include "openvic-extension/singletons/SoundSingleton.hpp" using namespace godot; using namespace OpenVic; @@ -27,6 +28,7 @@ static GameSingleton* _game_singleton = nullptr; static MenuSingleton* _menu_singleton = nullptr; static ModelSingleton* _model_singleton = nullptr; static AssetManager* _asset_manager_singleton = nullptr; +static SoundSingleton* _sound_singleton = nullptr; void initialize_openvic_types(ModuleInitializationLevel p_level) { if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { @@ -41,6 +43,10 @@ void initialize_openvic_types(ModuleInitializationLevel p_level) { _load_localisation = memnew(LoadLocalisation); Engine::get_singleton()->register_singleton("LoadLocalisation", LoadLocalisation::get_singleton()); + ClassDB::register_class<SoundSingleton>(); + _sound_singleton = memnew(SoundSingleton); + Engine::get_singleton()->register_singleton("SoundSingleton", SoundSingleton::get_singleton()); + ClassDB::register_class<GameSingleton>(); _game_singleton = memnew(GameSingleton); Engine::get_singleton()->register_singleton("GameSingleton", GameSingleton::get_singleton()); @@ -97,6 +103,9 @@ void uninitialize_openvic_types(ModuleInitializationLevel p_level) { Engine::get_singleton()->unregister_singleton("AssetManager"); memdelete(_asset_manager_singleton); + + Engine::get_singleton()->unregister_singleton("SoundSingleton"); + memdelete(_sound_singleton); } extern "C" { diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp index 4960b4f..5268789 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.cpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.cpp @@ -37,7 +37,9 @@ StringName const& GameSingleton::_signal_clock_state_changed() { void GameSingleton::_bind_methods() { OV_BIND_SMETHOD(setup_logger); - OV_BIND_METHOD(GameSingleton::load_defines_compatibility_mode, { "file_paths" }); + OV_BIND_METHOD(GameSingleton::load_defines_compatibility_mode); + OV_BIND_METHOD(GameSingleton::set_compatibility_mode_roots, { "file_paths" }); + OV_BIND_SMETHOD(search_for_game_path, { "hint_path" }, DEFVAL(String {})); OV_BIND_METHOD(GameSingleton::lookup_file_path, { "path" }); @@ -582,16 +584,21 @@ Error GameSingleton::_load_flag_sheet() { return ret; } -Error GameSingleton::load_defines_compatibility_mode(PackedStringArray const& file_paths) { +Error GameSingleton::set_compatibility_mode_roots(PackedStringArray const& file_paths) { Dataloader::path_vector_t roots; for (String const& path : file_paths) { roots.push_back(Utilities::godot_to_std_string(path)); } - Error err = OK; + ERR_FAIL_COND_V_MSG(!game_manager.set_roots(roots), FAILED, "Failed to set dataloader roots!"); + return OK; +} +Error GameSingleton::load_defines_compatibility_mode() { + Error err = OK; auto add_message = std::bind_front(&LoadLocalisation::add_message, LoadLocalisation::get_singleton()); - if (!game_manager.load_definitions(roots, add_message)) { + + if (!game_manager.load_definitions(add_message)) { UtilityFunctions::push_error("Failed to load defines!"); err = FAILED; } diff --git a/extension/src/openvic-extension/singletons/GameSingleton.hpp b/extension/src/openvic-extension/singletons/GameSingleton.hpp index e7f12bd..3c6d8f8 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.hpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.hpp @@ -71,7 +71,8 @@ namespace OpenVic { /* Load the game's defines in compatiblity mode from the filepath * pointing to the defines folder. */ - godot::Error load_defines_compatibility_mode(godot::PackedStringArray const& file_paths); + godot::Error set_compatibility_mode_roots(godot::PackedStringArray const& file_paths); + godot::Error load_defines_compatibility_mode(); static godot::String search_for_game_path(godot::String const& hint_path = {}); godot::String lookup_file_path(godot::String const& path) const; diff --git a/extension/src/openvic-extension/singletons/SoundSingleton.cpp b/extension/src/openvic-extension/singletons/SoundSingleton.cpp new file mode 100644 index 0000000..a32d9fe --- /dev/null +++ b/extension/src/openvic-extension/singletons/SoundSingleton.cpp @@ -0,0 +1,413 @@ +#include "SoundSingleton.hpp" + +#include <string_view> +#include <vector> + +#include <godot_cpp/core/error_macros.hpp> +#include <godot_cpp/variant/string.hpp> +#include <godot_cpp/classes/dir_access.hpp> +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/classes/stream_peer_buffer.hpp> + +#include "openvic-simulation/utility/StringUtils.hpp" +#include <openvic-extension/utility/Utilities.hpp> +#include <openvic-extension/utility/ClassBindings.hpp> +#include <openvic-extension/singletons/GameSingleton.hpp> +#include <openvic-dataloader/v2script/AbstractSyntaxTree.hpp> +#include <openvic-simulation/dataloader/Dataloader.hpp> +#include <openvic-simulation/dataloader/NodeTools.hpp> + +using OpenVic::Utilities::godot_to_std_string; +using OpenVic::Utilities::std_to_godot_string; + +using namespace godot; +using namespace OpenVic; +using namespace OpenVic::NodeTools; + +//ov_bind_method is used to make a method visible to godot +void SoundSingleton::_bind_methods() { + OV_BIND_METHOD(SoundSingleton::load_music); + OV_BIND_METHOD(SoundSingleton::get_song, {"song_name"}); + OV_BIND_METHOD(SoundSingleton::get_song_list); + + ADD_PROPERTY(PropertyInfo( + Variant::ARRAY, + "song_list", PROPERTY_HINT_ARRAY_TYPE, + "AudioStreamMP3"), + "", "get_song_list"); + + OV_BIND_METHOD(SoundSingleton::load_sounds); + OV_BIND_METHOD(SoundSingleton::get_sound_stream, {"sound_name"}); + OV_BIND_METHOD(SoundSingleton::get_sound_base_volume, {"sound_name"}); + OV_BIND_METHOD(SoundSingleton::get_sound_list); + + ADD_PROPERTY(PropertyInfo( + Variant::ARRAY, + "sound_list", + PROPERTY_HINT_ARRAY_TYPE, + "AudioStreamWAV"), + "", "get_sound_list"); + + OV_BIND_METHOD(SoundSingleton::load_title_theme); + OV_BIND_METHOD(SoundSingleton::get_title_theme); + + ADD_PROPERTY(PropertyInfo( + Variant::STRING, + "title_theme"), + "", "get_title_theme"); + +} + +SoundSingleton* SoundSingleton::get_singleton() { + return _singleton; +} + +SoundSingleton::SoundSingleton() { + ERR_FAIL_COND(_singleton != nullptr); + _singleton = this; +} + +SoundSingleton::~SoundSingleton() { + ERR_FAIL_COND(_singleton != this); + _singleton = nullptr; +} + + +//Load a sound from the path +Ref<AudioStreamMP3> SoundSingleton::_load_godot_mp3(String const& path) const { + const Ref<FileAccess> file = FileAccess::open(path, FileAccess::ModeFlags::READ); + + Error err = FileAccess::get_open_error(); + ERR_FAIL_COND_V_MSG( + err != OK || file.is_null(), nullptr, + vformat("Failed to open mp3 file %s", path) //named %s, path, + ); + + const PackedByteArray data = file->get_buffer(file->get_length()); + + Ref<AudioStreamMP3> sound = Ref<AudioStreamMP3>(); + sound.instantiate(); + sound->set_data(data); + + return sound; +} + +//slices a path down to after the base_folder, keeps the extension +//this is because the defines refer to audio files using this format, +//so we might as well use this form as the key for the "name"->audiostream map +String SoundSingleton::to_define_file_name(String const& path, std::string_view const& base_folder) const { + String name = path.replace("\\","/"); + return name.get_slice(base_folder.data(),1); //get file name with extension +} + +//Load a sound from the cache, or directly if its not in the cache +//take in a path, extract just the file name for the cache (and defines) +Ref<AudioStreamMP3> SoundSingleton::get_song(String const& path){ + String name = to_define_file_name(path, music_folder); + + const song_asset_map_t::const_iterator it = tracks.find(name); + if (it != tracks.end()) { //it->first = key, it->second = value + return it->second; + } + + const Ref<AudioStreamMP3> song = _load_godot_mp3(path); + + ERR_FAIL_NULL_V_MSG( + song, nullptr, + vformat("Failed to load music file: %s", path) + ); + tracks.emplace(std::move(name), song); + + return song; + +} + +//loading music is actually one of the slower things to do, and we want the title theme +//playing the whole time. Solution: load it first and separately +bool SoundSingleton::load_title_theme(){ + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V_MSG(game_singleton, false, vformat("Error retrieving GameSingleton")); + + static constexpr std::string_view music_directory = "music"; + bool ret = false; + + Dataloader::path_vector_t music_files = game_singleton->get_dataloader() + .lookup_files_in_dir_recursive(music_directory, ".mp3"); + + if(music_files.size() < 1){ + Logger::error("failed to load title theme: no files in music directory"); + } + + for(std::filesystem::path const& file_name : music_files) { + //the path + String file = std_to_godot_string(file_name.string()); + //file name + String file_stem = to_define_file_name(file,music_folder); + + if(file_stem == title_theme_name.data()){ + if(!get_song(file).is_valid()){ + Logger::error("failed to load title theme song at path ",file_name); + break; //don't try to append a null pointer to the list + } + else{ + String name = to_define_file_name(file,music_folder); + title_theme = name; + ret = true; + break; + } + } + + } + + if(!ret) Logger::error("Failed to load title theme!"); + + return ret; +} + +bool SoundSingleton::load_music() { + + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V_MSG(game_singleton, false, vformat("Error retrieving GameSingleton")); + + static constexpr std::string_view music_directory = "music"; + bool ret = true; + + Dataloader::path_vector_t music_files = game_singleton->get_dataloader() + .lookup_files_in_dir_recursive(music_directory, ".mp3"); + + if(music_files.size() < 1){ + Logger::error("failed to load music: no files in music directory"); + ret = false; + } + + for(std::filesystem::path const& file_name : music_files) { + String file = std_to_godot_string(file_name.string()); + String name = to_define_file_name(file,music_folder); + if(name == title_theme_name.data()) continue; + + if(!get_song(file).is_valid()){ + Logger::error("failed to load song at path ",file_name); + ret = false; + continue; //don't try to append a null pointer to the list + } + song_list.append(name); + + } + + return ret; +} + +//Load a sound into the sound cache, accessed via its file path +Ref<AudioStreamWAV> SoundSingleton::get_sound(String const& path){ + String name = to_define_file_name(path, sound_folder); + + const sfx_asset_map_t::const_iterator it = sfx.find(name); + if (it != sfx.end()) { //it->first = key, it->second = value + return it->second; + } + + const Ref<AudioStreamWAV> sound = _load_godot_wav(path); + + ERR_FAIL_NULL_V_MSG( + sound, nullptr, + vformat("Failed to load sound file %s", path) //named %s, path, + ); + + sfx.emplace(std::move(name), sound); + return sound; +} + +//Get a sound by its define name +Ref<AudioStreamWAV> SoundSingleton::get_sound_stream(String const& path) { + if(sfx_define[path].audioStream.has_value()){ + return sfx_define[path].audioStream.value(); + } + + ERR_FAIL_V_MSG( + nullptr, + vformat("Attempted to retrieve sound stream at invalid index ", path) + ); + +} + +//get the base volume of a sound from its define name +float SoundSingleton::get_sound_base_volume(String const& path) { + if(sfx_define[path].volume.has_value()){ + return sfx_define[path].volume.value().to_float(); + } + return 1.0; +} + +//Requires the sound defines to already be loaded by the dataloader +//then build the define map (define_identifier->{audiostream,volume}) +bool SoundSingleton::load_sounds() { + static constexpr std::string_view sound_directory = "sound"; + bool ret = true; + + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V_MSG(game_singleton, false, vformat("Error retrieving GameSingleton")); + + SoundEffectManager const& sound_manager = game_singleton->get_definition_manager().get_sound_effect_manager(); + + if(sound_manager.sound_effects_empty()){ + Logger::error("failed to load music: no identifiers in sounds.sfx"); + ret = false; + } + + for(SoundEffect const& sound_inst : sound_manager.get_sound_effects()){ + std::string folder_path = StringUtils::append_string_views(sound_directory, "/", sound_inst.get_file()); + fs::path full_path = game_singleton->get_dataloader().lookup_file(folder_path, false); + + //UI_Cavalry_Selected.wav doesn't exist (paradox mistake, UI_Cavalry_Select.wav does), just keep going + //the define its associated with also isn't used in game + if(full_path.empty()){ + Logger::warning("The sound define ",sound_inst.get_identifier()," points to an non-existing file ", folder_path); + continue; + } + + Ref<AudioStreamWAV> stream = get_sound(std_to_godot_string(full_path.string())); + if(stream.is_null()){ + Logger::error("failed to load sound ",sound_inst.get_identifier()," at path ",full_path); + ret = false; + continue; //don't try to append a null pointer to the list + } + + String name = to_define_file_name(std_to_godot_string(full_path.string()), sound_folder); + + StringName define_gd_name = std_to_godot_string(sound_inst.get_identifier()); + sfx_define[define_gd_name].audioStream = get_sound(name); + sfx_define[define_gd_name].volume = sound_inst.get_volume(); + + sound_list.append(define_gd_name); + } + + return ret; + +} + +Ref<AudioStreamWAV> SoundSingleton::_load_godot_wav(String const& path) const { + const Ref<FileAccess> file = FileAccess::open(path, FileAccess::ModeFlags::READ); + + Error err = FileAccess::get_open_error(); + ERR_FAIL_COND_V_MSG( + err != OK || file.is_null(), nullptr, + vformat("Failed to open wav file %s", path) + ); + + + Ref<AudioStreamWAV> sound = Ref<AudioStreamWAV>(); + sound.instantiate(); + + //RIFF file header + String riff_id = read_riff_str(file); //RIFF + int riff_size = std::min(static_cast<uint64_t>(file->get_32()), file->get_length()); + String form_type = read_riff_str(file); //WAVE + + + //ie. 16, 24, 32 bit audio + int bits_per_sample = 0; + + //godot audiostreamwav has: data,format,loop_begin,loop_end,loop_mode,mix_rate,stereo + + //RIFF reader + while(file->get_position() < riff_size){ + String id = read_riff_str(file); + int size = file->get_32(); + if(id=="LIST"){ + String list_type = read_riff_str(file); + } + else if(id=="JUNK"){ + const PackedByteArray junk = file->get_buffer(size); + } + else if(id=="fmt "){ + //what fields to read depends on the fmt chunk variant (can be 16, 18, or 40 bytes long) + //basic fields + + //2bytes: type of format can be 1=PCM, 3=IEEE float, 6=8bit Alaw, 7=8bit mu-law, FFFE=go by subformat + int formatTag = file->get_16(); + int channels = file->get_16(); + int samplesPerSec = file->get_32(); + int avgBytesPerSec = file->get_32(); + int blockAlign = file->get_16(); + + bits_per_sample = file->get_16(); + ERR_FAIL_COND_V_MSG( + bits_per_sample == 24 || bits_per_sample == 32, nullptr, + vformat("Unsupported wav file sample rate %s", bits_per_sample) + ); + + if(size > 16){ + int extensionSize = file->get_16(); + } + if(size > 18){ + //extensible format + int validBitsPerSample = file->get_16(); + int channelMask = file->get_32(); + + //16 byte subformat + int subFormat = file->get_16(); + String subFormatString = read_riff_str(file,14); + } + + //set godot properties + sound->set_stereo(channels==2); + switch(formatTag){ //TODO: verify, looks from 1 doc like these should be 0x0101, 0x102, ... + case 0:{ + sound->set_format(sound->FORMAT_8_BITS); + break; + } + case 1:{ + sound->set_format(sound->FORMAT_16_BITS); + break; + } + case 2:{ + sound->set_format(sound->FORMAT_IMA_ADPCM); + break; + } + default:{ + Logger::warning("unknown WAV format tag %x",formatTag); + sound->set_format(sound->FORMAT_16_BITS); + break; + } + } + + sound->set_mix_rate(samplesPerSec); + + } + else if(id=="data"){ + PackedByteArray audio_data = file->get_buffer(size); + + if(bits_per_sample == 24 || bits_per_sample == 32){ + //sound->set_data(to_16bit_wav_data(audio_data,bits_per_sample)); + Logger::error("WAV file ",godot_to_std_string(path), " uses an unsupported sample rate ", bits_per_sample); + } + else{ + sound->set_data(audio_data); + } + } + else if(id=="fact"){ //for compressed formats that aren't PCM + //TODO: Handle these other cases + int sampleLen = file->get_32(); //# samples/channel + Logger::warning("WAV fact header, indicates likely unhandled case"); + } + else{ + //Logger::warning("skipping Unhandled RIFF chunk of id ",godot_to_std_string(id)); + //known chunks that cause this: "smpl", "labl", "cue ", "ltxt", info chunks (IART, ICOP, IENG, ...) + //these don't seem to be needed for our uses + const PackedByteArray junk = file->get_buffer(size); //just try and skip this chunk + + } + if(file->get_position() % 2 != 0){ //align to even bytes + file->get_8(); + } + + } + + sound->set_loop_end(file->get_length()/4); + return sound; +} + +//set size if its an info string, otherwise leaving +String SoundSingleton::read_riff_str(Ref<FileAccess> const& file, int size) const { + return file->get_buffer(size).get_string_from_ascii(); +}
\ No newline at end of file diff --git a/extension/src/openvic-extension/singletons/SoundSingleton.hpp b/extension/src/openvic-extension/singletons/SoundSingleton.hpp new file mode 100644 index 0000000..bfa03ea --- /dev/null +++ b/extension/src/openvic-extension/singletons/SoundSingleton.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include <godot_cpp/core/class_db.hpp> +#include <godot_cpp/core/object.hpp> +#include <godot_cpp/variant/string.hpp> +#include <godot_cpp/variant/string_name.hpp> + +#include <godot_cpp/classes/file_access.hpp> +#include <godot_cpp/classes/audio_stream.hpp> +#include <godot_cpp/classes/audio_stream_mp3.hpp> +#include <godot_cpp/classes/audio_stream_wav.hpp> +#include <godot_cpp/templates/vector.hpp> + +#include <openvic-simulation/types/OrderedContainers.hpp> +#include <openvic-simulation/types/IdentifierRegistry.hpp> +#include <openvic-simulation/types/fixed_point/FixedPoint.hpp> + +namespace OpenVic { + + class SoundSingleton : public godot::Object { + + GDCLASS(SoundSingleton, godot::Object); + + static inline SoundSingleton* _singleton = nullptr; + + //cache of songs + //names will be like "subfolder/songname", with "music/" base folder and the extension (.mp3) being excluded + using song_asset_map_t = deque_ordered_map<godot::StringName, godot::Ref<godot::AudioStreamMP3>>; + song_asset_map_t tracks; + + //cache of sfx (map file name to an audio stream), only used temporarily until the sfx_define_map is built + using sfx_asset_map_t = deque_ordered_map<godot::StringName, godot::Ref<godot::AudioStreamWAV>>; + sfx_asset_map_t sfx; + + //define name, stream ref, volume for sound effects so we can get these properties with a simple call in godot + struct sound_asset_t { + std::optional<godot::Ref<godot::AudioStreamWAV>> audioStream; + std::optional<fixed_point_t> volume; + }; + using sfx_define_map_t = deque_ordered_map<godot::StringName,sound_asset_t>; + sfx_define_map_t sfx_define; + + static constexpr std::string_view title_theme_name = "thecoronation_titletheme.mp3"; + static constexpr std::string_view music_folder = "music/"; + static constexpr std::string_view sound_folder = "sound/"; + + //property for gd scripts to access song names + godot::Array PROPERTY(song_list); + godot::String PROPERTY(title_theme); + + //property for gd scripts to access sound names + godot::Array PROPERTY(sound_list); + + public: + SoundSingleton(); + ~SoundSingleton(); + static SoundSingleton* get_singleton(); + + protected: + static void _bind_methods(); + + godot::String to_define_file_name(godot::String const& path, std::string_view const& base_folder) const; + godot::String read_riff_str(godot::Ref<godot::FileAccess> const& file, int size=4) const; + + private: + /* Loads AudioStreams (.mp3 or .wav) at runtime using godot's functions*/ + godot::Ref<godot::AudioStreamMP3> _load_godot_mp3(godot::String const& path) const; + godot::Ref<godot::AudioStreamWAV> _load_godot_wav(godot::String const& path) const; + + public: + //gets a song from the cache ('tracks' variable), or if not, then from the files using _load_godot_mp3 + godot::Ref<godot::AudioStreamMP3> get_song(godot::String const& name); + godot::Ref<godot::AudioStreamWAV> get_sound(godot::String const& path); + + //load the files into memory + bool load_music(); + bool load_sounds(); + bool load_title_theme(); + + //for sound effects, get the stream and relative volume it should play at from the sfx map + godot::Ref<godot::AudioStreamWAV> get_sound_stream(godot::String const& path); + float get_sound_base_volume(godot::String const& path); + + }; + +}
\ No newline at end of file |