aboutsummaryrefslogtreecommitdiff
path: root/extension/src/openvic-extension/singletons/SoundSingleton.cpp
blob: b9a2ee3acfb2c24c45cc17b64aed34f44e4fa133 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
#include "SoundSingleton.hpp"

#include <string_view>

#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();
}