diff options
author | Nemrav <> | 2024-04-22 22:30:21 +0200 |
---|---|---|
committer | Nemrav <> | 2024-08-06 01:40:34 +0200 |
commit | 9506f4160f0bd351f0853e6e8263ea927d9ec771 (patch) | |
tree | 0a9bd4f52c01315c3b38ce641a78c33bd8562be2 /extension/src/openvic-extension | |
parent | fde15e554dc9ed458a838683c69d10262764db12 (diff) |
Music and Sound Effect loading and playing
almost working music loading
music list sent to godot
wav loading not working
mp3 metadata
load wav initial
load and play title theme first
fix errors not related to nodetools
working wav define loading and music playlists
fixup error handling and playlist
load song chances
code style progress
switch mp3 metadata addon to MusicMetadata
add id3v1 mp3 metadata handling to MusicMetadata
remove commented code from id3v1
sounds gd styling
fix dataloader conflicts
clean up commented code
move MusicChance to dataloader
remove reference to old addon
move sfx defines loader
add self to credits
feedback on soundSingleton
include subfolders in sound singleton sfx map
replace space tabs
replace std_view_to_godot_string with std_to_godot_string
revise singleton files
revise gd side sound
final revisions
Diffstat (limited to 'extension/src/openvic-extension')
5 files changed, 521 insertions, 5 deletions
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 |