| 1 | // SuperTux |
| 2 | // Copyright (C) 2006 Matthias Braun <matze@braunis.de> |
| 3 | // |
| 4 | // This program is free software: you can redistribute it and/or modify |
| 5 | // it under the terms of the GNU General Public License as published by |
| 6 | // the Free Software Foundation, either version 3 of the License, or |
| 7 | // (at your option) any later version. |
| 8 | // |
| 9 | // This program is distributed in the hope that it will be useful, |
| 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | // GNU General Public License for more details. |
| 13 | // |
| 14 | // You should have received a copy of the GNU General Public License |
| 15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | |
| 17 | /** Used SDL_mixer and glest source as reference */ |
| 18 | |
| 19 | #include "audio/sound_file.hpp" |
| 20 | |
| 21 | #include <config.h> |
| 22 | |
| 23 | #include <string.h> |
| 24 | #include <physfs.h> |
| 25 | #include <sstream> |
| 26 | #include <unordered_map> |
| 27 | |
| 28 | #include "audio/ogg_sound_file.hpp" |
| 29 | #include "audio/sound_error.hpp" |
| 30 | #include "audio/wav_sound_file.hpp" |
| 31 | #include "util/file_system.hpp" |
| 32 | #include "util/reader_document.hpp" |
| 33 | #include "util/reader_mapping.hpp" |
| 34 | #include "util/string_util.hpp" |
| 35 | |
| 36 | namespace { |
| 37 | |
| 38 | const std::string& get_fallback_path(const std::string& file_path); |
| 39 | |
| 40 | ReaderDocument doc_from_file_fallback(std::string& filename) |
| 41 | { |
| 42 | try { |
| 43 | return ReaderDocument::from_file(filename); |
| 44 | } catch(const std::exception&) { |
| 45 | filename = get_fallback_path(filename); |
| 46 | return ReaderDocument::from_file(filename); |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | std::unique_ptr<SoundFile> load_music_file(const std::string& filename_original) |
| 51 | { |
| 52 | // filename may be changed by doc_from_file_fallback |
| 53 | std::string filename = filename_original; |
| 54 | auto doc = doc_from_file_fallback(filename); |
| 55 | auto root = doc.get_root(); |
| 56 | if (root.get_name() != "supertux-music" ) |
| 57 | { |
| 58 | throw SoundError("file is not a supertux-music file." ); |
| 59 | } |
| 60 | else |
| 61 | { |
| 62 | auto music = root.get_mapping(); |
| 63 | |
| 64 | std::string raw_music_file; |
| 65 | float loop_begin = 0; |
| 66 | float loop_at = -1; |
| 67 | |
| 68 | music.get("file" , raw_music_file); |
| 69 | music.get("loop-begin" , loop_begin); |
| 70 | music.get("loop-at" , loop_at); |
| 71 | |
| 72 | if (loop_begin < 0) { |
| 73 | throw SoundError("can't loop from negative value" ); |
| 74 | } |
| 75 | |
| 76 | std::string basedir = FileSystem::dirname(filename); |
| 77 | raw_music_file = FileSystem::normalize(basedir + raw_music_file); |
| 78 | |
| 79 | auto file = PHYSFS_openRead(raw_music_file.c_str()); |
| 80 | if (!file) { |
| 81 | std::stringstream msg; |
| 82 | msg << "Couldn't open '" << raw_music_file << "': " << PHYSFS_getLastErrorCode(); |
| 83 | throw SoundError(msg.str()); |
| 84 | } |
| 85 | auto format = SoundFile::get_file_format(file, raw_music_file); |
| 86 | if (format == SoundFile::FORMAT_WAV) |
| 87 | { |
| 88 | return std::make_unique<WavSoundFile>(file); |
| 89 | } |
| 90 | else |
| 91 | { |
| 92 | return std::make_unique<OggSoundFile>(file, loop_begin, loop_at); |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | } // namespace |
| 98 | |
| 99 | std::unique_ptr<SoundFile> load_sound_file(const std::string& filename) |
| 100 | { |
| 101 | if (StringUtil::has_suffix(filename, ".music" )) { |
| 102 | return load_music_file(filename); |
| 103 | } |
| 104 | |
| 105 | auto file = PHYSFS_openRead(filename.c_str()); |
| 106 | if (!file) { |
| 107 | file = PHYSFS_openRead(get_fallback_path(filename).c_str()); |
| 108 | if (!file) { |
| 109 | std::stringstream msg; |
| 110 | msg << "Couldn't open '" << filename << "': " << |
| 111 | PHYSFS_getLastErrorCode() << ", using dummy sound file." ; |
| 112 | throw SoundError(msg.str()); |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | auto format = SoundFile::get_file_format(file, filename); |
| 117 | if (format == SoundFile::FORMAT_WAV) |
| 118 | { |
| 119 | return std::make_unique<WavSoundFile>(file); |
| 120 | } |
| 121 | else |
| 122 | { |
| 123 | return std::make_unique<OggSoundFile>(file, 0, -1); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | SoundFile::FileFormat |
| 128 | SoundFile::get_file_format(PHYSFS_File* file, const std::string& filename) |
| 129 | { |
| 130 | try { |
| 131 | char magic[4]; |
| 132 | if (PHYSFS_readBytes(file, magic, sizeof(magic)) < static_cast<std::make_signed<size_t>::type>(sizeof(magic))) |
| 133 | throw SoundError("Couldn't read magic, file too short" ); |
| 134 | if (PHYSFS_seek(file, 0) == 0) { |
| 135 | std::stringstream msg; |
| 136 | msg << "Couldn't seek through sound file: " << PHYSFS_getLastErrorCode(); |
| 137 | throw SoundError(msg.str()); |
| 138 | } |
| 139 | |
| 140 | if (strncmp(magic, "RIFF" , 4) == 0) |
| 141 | return FileFormat::FORMAT_WAV; |
| 142 | else if (strncmp(magic, "OggS" , 4) == 0) |
| 143 | return FileFormat::FORMAT_OGG; |
| 144 | else |
| 145 | throw SoundError("Unknown file format" ); |
| 146 | } catch(std::exception& e) { |
| 147 | std::stringstream msg; |
| 148 | msg << "Couldn't read '" << filename << "': " << e.what(); |
| 149 | throw SoundError(msg.str()); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | |
| 154 | // Legacy |
| 155 | |
| 156 | namespace { |
| 157 | |
| 158 | // List obtained with the help of sed: |
| 159 | // find | sort | sed 's:^\.:/music:; /\./ !d; s:\(.*/\)\([^/]*$\):{"\2", "\1\2"},:g' |
| 160 | std::unordered_map<std::string, std::string> fallback_paths = { |
| 161 | {"airship_2.ogg" , "/music/antarctic/airship_2.ogg" }, |
| 162 | {"airship_remix-2.music" , "/music/antarctic/airship_remix-2.music" }, |
| 163 | {"airship_remix.music" , "/music/antarctic/airship_remix.music" }, |
| 164 | {"airship_remix.ogg" , "/music/antarctic/airship_remix.ogg" }, |
| 165 | {"arctic_breeze.music" , "/music/antarctic/arctic_breeze.music" }, |
| 166 | {"arctic_breeze.ogg" , "/music/antarctic/arctic_breeze.ogg" }, |
| 167 | {"arctic_cave.music" , "/music/antarctic/arctic_cave.music" }, |
| 168 | {"arctic_cave.ogg" , "/music/antarctic/arctic_cave.ogg" }, |
| 169 | {"bossattack.music" , "/music/antarctic/bossattack.music" }, |
| 170 | {"bossattack.ogg" , "/music/antarctic/bossattack.ogg" }, |
| 171 | {"cave.music" , "/music/antarctic/cave.music" }, |
| 172 | {"cave.ogg" , "/music/antarctic/cave.ogg" }, |
| 173 | {"chipdisko.music" , "/music/antarctic/chipdisko.music" }, |
| 174 | {"chipdisko.ogg" , "/music/antarctic/chipdisko.ogg" }, |
| 175 | {"jewels.music" , "/music/antarctic/jewels.music" }, |
| 176 | {"jewels.ogg" , "/music/antarctic/jewels.ogg" }, |
| 177 | {"salcon.music" , "/music/antarctic/salcon.music" }, |
| 178 | {"salcon.ogg" , "/music/antarctic/salcon.ogg" }, |
| 179 | {"voc-boss.music" , "/music/antarctic/voc-boss.music" }, |
| 180 | {"voc-boss.ogg" , "/music/antarctic/voc-boss.ogg" }, |
| 181 | {"voc-dark.music" , "/music/antarctic/voc-dark.music" }, |
| 182 | {"voc-dark.ogg" , "/music/antarctic/voc-dark.ogg" }, |
| 183 | {"voc-daytime2.music" , "/music/antarctic/voc-daytime2.music" }, |
| 184 | {"voc-daytime2.ogg" , "/music/antarctic/voc-daytime2.ogg" }, |
| 185 | {"voc-daytime.music" , "/music/antarctic/voc-daytime.music" }, |
| 186 | {"voc-daytime.ogg" , "/music/antarctic/voc-daytime.ogg" }, |
| 187 | {"voc-night.music" , "/music/antarctic/voc-night.music" }, |
| 188 | {"voc-night.ogg" , "/music/antarctic/voc-night.ogg" }, |
| 189 | {"darkforestkeep.music" , "/music/castle/darkforestkeep.music" }, |
| 190 | {"darkforestkeep.ogg" , "/music/castle/darkforestkeep.ogg" }, |
| 191 | {"fortress.music" , "/music/castle/fortress.music" }, |
| 192 | {"fortress.ogg" , "/music/castle/fortress.ogg" }, |
| 193 | {"beneath_the_rabbit_hole.music" , "/music/forest/beneath_the_rabbit_hole.music" }, |
| 194 | {"beneath_the_rabbit_hole.ogg" , "/music/forest/beneath_the_rabbit_hole.ogg" }, |
| 195 | {"bright_thunders.music" , "/music/forest/bright_thunders.music" }, |
| 196 | {"bright_thunders.ogg" , "/music/forest/bright_thunders.ogg" }, |
| 197 | {"clavelian_march.music" , "/music/forest/clavelian_march.music" }, |
| 198 | {"clavelian_march.ogg" , "/music/forest/clavelian_march.ogg" }, |
| 199 | {"forest2.music" , "/music/forest/forest2.music" }, |
| 200 | {"forest2.ogg" , "/music/forest/forest2.ogg" }, |
| 201 | {"forest3.music" , "/music/forest/forest3.music" }, |
| 202 | {"forest3.ogg" , "/music/forest/forest3.ogg" }, |
| 203 | {"forest-cave.music" , "/music/forest/forest-cave.music" }, |
| 204 | {"forest-cave.ogg" , "/music/forest/forest-cave.ogg" }, |
| 205 | {"forest-map.music" , "/music/forest/forest-map.music" }, |
| 206 | {"forestmap.ogg" , "/music/forest/forestmap.ogg" }, |
| 207 | {"forest.music" , "/music/forest/forest.music" }, |
| 208 | {"forest.ogg" , "/music/forest/forest.ogg" }, |
| 209 | {"forest-sprint.music" , "/music/forest/forest-sprint.music" }, |
| 210 | {"forest-sprint.ogg" , "/music/forest/forest-sprint.ogg" }, |
| 211 | {"forest_theme.music" , "/music/forest/forest_theme.music" }, |
| 212 | {"forest_theme.ogg" , "/music/forest/forest_theme.ogg" }, |
| 213 | {"ghostforest2.music" , "/music/forest/ghostforest2.music" }, |
| 214 | {"ghostforest2.ogg" , "/music/forest/ghostforest2.ogg" }, |
| 215 | {"ghostforest_map.music" , "/music/forest/ghostforest_map.music" }, |
| 216 | {"ghostforest_map.ogg" , "/music/forest/ghostforest_map.ogg" }, |
| 217 | {"ghostforest.music" , "/music/forest/ghostforest.music" }, |
| 218 | {"ghostforest.ogg" , "/music/forest/ghostforest.ogg" }, |
| 219 | {"greatgigantic.music" , "/music/forest/greatgigantic.music" }, |
| 220 | {"greatgigantic.ogg" , "/music/forest/greatgigantic.ogg" }, |
| 221 | {"new_forest_map.music" , "/music/forest/new_forest_map.music" }, |
| 222 | {"new_forest_map.ogg" , "/music/forest/new_forest_map.ogg" }, |
| 223 | {"shallow-green.music" , "/music/forest/shallow-green.music" }, |
| 224 | {"shallow-green.ogg" , "/music/forest/shallow-green.ogg" }, |
| 225 | {"treeboss.music" , "/music/forest/treeboss.music" }, |
| 226 | {"treeboss.ogg" , "/music/forest/treeboss.ogg" }, |
| 227 | {"wisphunt.music" , "/music/forest/wisphunt.music" }, |
| 228 | {"wisphunt.ogg" , "/music/forest/wisphunt.ogg" }, |
| 229 | {"battle_theme.music" , "/music/misc/battle_theme.music" }, |
| 230 | {"battle_theme.ogg" , "/music/misc/battle_theme.ogg" }, |
| 231 | {"bonuscave.music" , "/music/misc/bonuscave.music" }, |
| 232 | {"bonuscave.ogg" , "/music/misc/bonuscave.ogg" }, |
| 233 | {"christmas_theme.music" , "/music/misc/christmas_theme.music" }, |
| 234 | {"christmas_theme.ogg" , "/music/misc/christmas_theme.ogg" }, |
| 235 | {"credits.music" , "/music/misc/credits.music" }, |
| 236 | {"credits.ogg" , "/music/misc/credits.ogg" }, |
| 237 | {"halloween_1.music" , "/music/misc/halloween_1.music" }, |
| 238 | {"halloween_1.ogg" , "/music/misc/halloween_1.ogg" }, |
| 239 | {"intro.music" , "/music/misc/intro.music" }, |
| 240 | {"intro.ogg" , "/music/misc/intro.ogg" }, |
| 241 | {"invincible.ogg" , "/music/misc/invincible.ogg" }, |
| 242 | {"leveldone.ogg" , "/music/misc/leveldone.ogg" }, |
| 243 | {"theme.music" , "/music/misc/theme.music" }, |
| 244 | {"theme.ogg" , "/music/misc/theme.ogg" }, |
| 245 | }; |
| 246 | |
| 247 | const std::string& get_fallback_path(const std::string& file_path) |
| 248 | { |
| 249 | std::string file_name = FileSystem::basename(file_path); |
| 250 | auto it = fallback_paths.find(file_name); |
| 251 | if (it != fallback_paths.end()) |
| 252 | return it->second; |
| 253 | // No fallback path found |
| 254 | return file_path; |
| 255 | } |
| 256 | |
| 257 | } // namespace |
| 258 | |
| 259 | |
| 260 | /* EOF */ |
| 261 | |