diff --git a/CMakeLists.txt b/CMakeLists.txt index e1e6590..fd0b85c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,16 @@ if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") cmake_policy(SET CMP0135 NEW) endif() +set(MINIAUDIO_VERSION "0.11.23" CACHE STRING "Version of miniaudio to use") +set(MINIAUDIO_NO_MP3 YES) +set(MINIAUDIO_NO_FLAC YES) +set(MINIAUDIO_NO_LIBVORBIS NO) +FetchContent_Declare( + miniaudio + URL https://github.com/mackron/miniaudio/archive/refs/tags/${MINIAUDIO_VERSION}.tar.gz +) +FetchContent_MakeAvailable(miniaudio) + set(SFSEXP_VERSION "1.4.1" CACHE STRING "Version of SfSexp to use") FetchContent_Declare( sfsexp @@ -39,18 +49,11 @@ FetchContent_Declare( FetchContent_MakeAvailable(sfsexp) include("${PROJECT_SOURCE_DIR}/cmake/sfsexp.cmake") -option(NEOTUX_USE_MIXER "Build with SDL3 Mixer support." ON) - -if (NEOTUX_USE_MIXER) - message(STATUS "Using SDL3 Mixer...") - FetchContent_Declare( - SDL3_mixer - URL https://github.com/libsdl-org/SDL_mixer/archive/68764f35899e133b402336843c046d75136eaf08.tar.gz - ) - FetchContent_MakeAvailable(SDL3_mixer) -else() - message(STATUS "NOT using SDL3 Mixer...") -endif (NEOTUX_USE_MIXER) +if (PSP) + message(STATUS "Building PSP version...") + set(NEOTUX_PSP ON) + set(NEOTUX_DATA_DIR "data") +endif() set(SSQ_BUILD_STATIC_ONLY Off) @@ -116,16 +119,18 @@ endif() add_executable(NeoTux ${NEOTUX_SRC} ) target_include_directories(NeoTux PRIVATE src tests/game_tests) target_link_libraries(NeoTux PUBLIC - SFSEXP SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image -# PkgConfig::SDL3_image -# PkgConfig::SDL3_ttf + + SFSEXP + + miniaudio + miniaudio_libvorbis ) -if(NEOTUX_USE_MIXER) - target_link_libraries(NeoTux PUBLIC SDL3_mixer::SDL3_mixer) -endif() + +# NOTE: They seem to have forgotten to add this include directory to the miniaudio_libvorbis target. +target_include_directories(NeoTux SYSTEM PUBLIC ${miniaudio_SOURCE_DIR}/extras/decoders/libvorbis) if(NEOTUX_BGFX) target_sources(NeoTux PRIVATE "${NEOTUX_DATA_DIR}/shaders/frag.glsl" "${NEOTUX_DATA_DIR}/shaders/vert.glsl") diff --git a/config.h.in b/config.h.in index 6f7be9b..103209e 100644 --- a/config.h.in +++ b/config.h.in @@ -1,5 +1,4 @@ #cmakedefine NEOTUX_BGFX -#cmakedefine NEOTUX_USE_MIXER #cmakedefine NEOTUX_PSP #cmakedefine NEOTUX_UNITTESTS_DIR "@NEOTUX_UNITTESTS_DIR@" #cmakedefine NEOTUX_DATA_DIR "@NEOTUX_DATA_DIR@" diff --git a/src/audio/mixer.cpp b/src/audio/mixer.cpp index 2285e79..47468b8 100644 --- a/src/audio/mixer.cpp +++ b/src/audio/mixer.cpp @@ -1,125 +1,177 @@ -// SuperTux -// Copyright (C) 2025 Hyland B. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License // along with this program. If not, see . -#ifdef NEOTUX_USE_MIXER -#include -#endif #include +#include +#include + +#include "mixer.hpp" #include "sdl_exception.hpp" #include "util/filesystem.hpp" #include "util/logger.hpp" -#include "mixer.hpp" +#include "audio/sound_manager.hpp" +#include "audio/music_reader.hpp" + +MAException::MAException(const std::string& what, int result) : + std::runtime_error(std::format("{} (ma error: {})", what, result)) +{} + +struct Mixer::Impl +{ + ma_engine engine; + ma_resource_manager resource_manager; + + ma_sound music; + MusicData music_data; +}; Mixer g_mixer; -#ifdef NEOTUX_USE_MIXER -Mixer::Mixer() : - m_music(nullptr, Mix_FreeMusic), - m_cache(), - m_current_channel(0) +Mixer::Mixer(): + impl(std::make_unique()) { - SDL_AudioSpec spec; - spec.freq = MIX_DEFAULT_FREQUENCY; - spec.format = MIX_DEFAULT_FORMAT; - spec.channels = MIX_DEFAULT_CHANNELS; - - SDL_Init(SDL_INIT_AUDIO); - Mix_Init(MIX_INIT_OGG | MIX_INIT_WAVPACK); - - Mix_OpenAudio(0, &spec); - Logger::info(std::format("Opened audio at {}Hz, {} bit{}, {} audio buffer", - spec.freq, - spec.format & 0xFF, - SDL_AUDIO_ISFLOAT(spec.format) ? " (float)" : "", - (spec.channels > 2) ? "surround" : (spec.channels > 1) ? "stereo" : "mono")); + ma_result result; + + ma_decoding_backend_vtable* decoders[] = { + ma_decoding_backend_libvorbis + }; + + ma_resource_manager_config resource_manager_cfg = ma_resource_manager_config_init(); + resource_manager_cfg.pCustomDecodingBackendUserData = nullptr; + resource_manager_cfg.ppCustomDecodingBackendVTables = decoders; + resource_manager_cfg.customDecodingBackendCount = sizeof(decoders) / sizeof(decoders[0]); + + result = ma_resource_manager_init(&resource_manager_cfg, &impl->resource_manager); + if (result != MA_SUCCESS) + { + throw MAException("Failed to initialize resource manager", result); + } + + ma_engine_config engine_cfg; + engine_cfg = ma_engine_config_init(); + engine_cfg.pResourceManager = &impl->resource_manager; + + result = ma_engine_init(&engine_cfg, &impl->engine); + if (result != MA_SUCCESS) + { + throw MAException("Failed to initialize engine", result); + } + + ma_device* dev; + ma_device_info dev_info; + dev = ma_engine_get_device(&impl->engine); + ma_device_get_info(dev, ma_device_type_playback, &dev_info); + + Logger::info("Mixer", "Opened audio device:"); + Logger::info("Mixer", std::format("\tSample rate: {}Hz", + dev_info.nativeDataFormats[0].sampleRate)); + Logger::info("Mixer", std::format("\tChannels: {} sound ({})", + get_channels_name(dev_info.nativeDataFormats[0].channels), + dev_info.nativeDataFormats[0].channels)); + Logger::info("Mixer", std::format("\tFormat: {}", + ma_get_format_name(dev_info.nativeDataFormats[0].format))); } -#else -Mixer::Mixer() { - Logger::info("Built without SDL3 Mixer support. Hope you enjoy crickets."); + +std::string Mixer::get_channels_name(u32 channels) +{ + if (channels > 2) + return "surround"; + else if (channels > 1) + return "stereo"; + else + return "mono"; } -#endif void Mixer::shutdown() { -#ifdef NEOTUX_USE_MIXER - m_music.reset(); - m_cache.clear(); - //m_soundcache.clear(); -#endif + ma_sound_uninit(&impl->music); + ma_engine_uninit(&impl->engine); + ma_resource_manager_uninit(&impl->resource_manager); } bool Mixer::is_playing_music() { -#ifdef NEOTUX_USE_MIXER - return Mix_PlayingMusic(); -#else - return true; -#endif + return ma_sound_is_playing(&impl->music); } void Mixer::stop_playing_music() { -#ifdef NEOTUX_USE_MIXER - Mix_HaltMusic(); -#endif + ma_sound_stop(&impl->music); +} + +ma_engine* +Mixer::engine() +{ + return &impl->engine; } -// TODO Cache sounds void Mixer::play_sound(const std::string &filename) { -#ifdef NEOTUX_USE_MIXER - Mix_Chunk *chunk; - if (m_cache.contains(filename)) - { - chunk = m_cache.at(filename).get(); - } - else { - chunk = Mix_LoadWAV(FS::path(filename).c_str()); - m_cache.insert({filename, std::unique_ptr(chunk, Mix_FreeChunk)}); + ma_sound* sound = g_sound_manager.load(filename); + play_sound(sound); +} + +void +Mixer::play_sound(ma_sound* sound) +{ + if (ma_sound_is_playing(sound)) + ma_sound_stop(sound); + + ma_result result = ma_sound_start(sound); + if (result != MA_SUCCESS) { + throw MAException("Failed to play sound", result); } - - if (!chunk) - throw SDLException("Couldn't load chunk"); - - ++m_current_channel; - if (m_current_channel == 4) - m_current_channel = 0; - Mix_PlayChannel(m_current_channel, chunk, false); -#endif } void -Mixer::play_music(const std::string &filename) +Mixer::play_music(std::string filename) { -#ifdef NEOTUX_USE_MIXER - Mix_Music *music; - music = Mix_LoadMUS(FS::path(filename).c_str()); - - if (!music) + if (filename.ends_with(".music")) { - throw SDLException("Couldn't load music"); + MusicReader reader; + impl->music_data = reader.open(FS::path(filename)); + filename = FS::join(FS::parent_dir(filename), impl->music_data.file); + } else { + impl->music_data = {}; } - - Mix_FadeInMusic(music, true, 2000); - - m_music.reset(music); -#endif -} + ma_result result; + ma_sound_uninit(&impl->music); + result = ma_sound_init_from_file(&impl->engine, FS::path(filename).c_str(), + MA_SOUND_FLAG_STREAM, nullptr, nullptr, &impl->music); + + if (result != MA_SUCCESS) { + throw MAException(std::format("Failed to load music {}", FS::path(filename)), result); + } + + ma_data_source* music_source = ma_sound_get_data_source(&impl->music); + ma_uint32 samplerate; + ma_data_source_get_data_format(music_source, nullptr, nullptr, &samplerate, nullptr, 67); + + ma_data_source_set_loop_point_in_pcm_frames(music_source, impl->music_data.loop_begin * samplerate, + impl->music_data.loop_at * samplerate); + ma_sound_set_looping(&impl->music, MA_TRUE); + + result = ma_sound_start(&impl->music); + if (result != MA_SUCCESS) { + throw MAException(std::format("Failed to play music {}", FS::path(filename)), result); + } + +} diff --git a/src/audio/mixer.hpp b/src/audio/mixer.hpp index 2adb281..e43fcc6 100644 --- a/src/audio/mixer.hpp +++ b/src/audio/mixer.hpp @@ -1,49 +1,57 @@ -// SuperTux -// Copyright (C) 2025 Hyland B. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License +// SuperTux +// Copyright (C) 2025 Hyland B. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License // along with this program. If not, see . #ifndef SUPERTUX_SRC_AUDIO_MIXER_HPP #define SUPERTUX_SRC_AUDIO_MIXER_HPP #include -#include -#include -#include -#include "config.h" -#ifdef NEOTUX_USE_MIXER -#include -#endif +#include "types.hpp" + +class MAException : public std::runtime_error +{ +public: + MAException(const std::string &what, i32 result); +}; + +struct ma_engine; +struct ma_sound; class Mixer { + friend class SoundManager; + +public: + static std::string get_channels_name(u32 channels); + public: Mixer(); ~Mixer() = default; - + void shutdown(); - void play_sound(const std::string &filename); - void play_music(const std::string &filename); + void play_sound(const std::string& filename); + void play_sound(ma_sound* sound); + void play_music(std::string filename); bool is_playing_music(); void stop_playing_music(); + + ma_engine* engine(); + private: -#ifdef NEOTUX_USE_MIXER - std::unordered_map> m_cache; - std::unique_ptr m_music; - int m_current_channel; -#endif - //std::vector> m_soundcache; + struct Impl; + std::unique_ptr impl; }; extern Mixer g_mixer; diff --git a/src/audio/music_data.hpp b/src/audio/music_data.hpp new file mode 100644 index 0000000..021effc --- /dev/null +++ b/src/audio/music_data.hpp @@ -0,0 +1,29 @@ +// SuperTux +// Copyright (C) 2025 MatusGuy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +#ifndef SUPERTUX_SRC_AUDIO_MUSIC_DATA_HPP +#define SUPERTUX_SRC_AUDIO_MUSIC_DATA_HPP + +#include + +#include "types.hpp" + +struct MusicData { + std::string file; + u32 loop_begin = 0; + u32 loop_at = UINT_MAX; +}; + +#endif // SUPERTUX_SRC_AUDIO_MUSIC_DATA_HPP diff --git a/src/audio/music_reader.cpp b/src/audio/music_reader.cpp new file mode 100644 index 0000000..3af60ea --- /dev/null +++ b/src/audio/music_reader.cpp @@ -0,0 +1,53 @@ +// SuperTux +// Copyright (C) 2025 MatusGuy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +#include "music_reader.hpp" + +MusicReader::MusicReader() +{ + +} + +MusicData +MusicReader::open(const std::string& filename) +{ + SexpElt root; + MusicData out; + + root = m_parser.read_file(filename); + + if (!root.is_list()) + return {}; + + root = root.get_list(); + if (root.get_value() == "supertux-music") + Logger::debug("If it walks like a supertux-music and quacks like a supertux-music..."); + + SexpElt elt; + elt = root.find_car("file"); + if (!elt.is_valid()) + Logger::fatal("MusicReader", "File not specified"); + out.file = elt.next().get_value(); + + elt = root.find_car("loop-begin"); + if (elt.is_valid()) + out.loop_begin = elt.next().get_int_or(out.loop_begin); + + elt = root.find_car("loop-at"); + if (elt.is_valid()) + out.loop_at = elt.next().get_int_or(out.loop_at); + + return out; +} diff --git a/src/audio/music_reader.hpp b/src/audio/music_reader.hpp new file mode 100644 index 0000000..46aa3d0 --- /dev/null +++ b/src/audio/music_reader.hpp @@ -0,0 +1,33 @@ +// SuperTux +// Copyright (C) 2025 MatusGuy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +#ifndef SUPERTUX_SRC_AUDIO_MUSIC_READER_HPP +#define SUPERTUX_SRC_AUDIO_MUSIC_READER_HPP + +#include "util/sexp.hpp" +#include "util/logger.hpp" +#include "audio/music_data.hpp" + +class MusicReader +{ +public: + MusicReader(); + + MusicData open(const std::string &filename); +private: + SexpParser m_parser; +}; + +#endif diff --git a/src/audio/sound_manager.cpp b/src/audio/sound_manager.cpp new file mode 100644 index 0000000..b99c95e --- /dev/null +++ b/src/audio/sound_manager.cpp @@ -0,0 +1,49 @@ +// SuperTux +// Copyright (C) 2025 MatusGuy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include + +#include "sound_manager.hpp" +#include "audio/mixer.hpp" +#include "util/filesystem.hpp" + +struct SoundManager::Impl { + std::unordered_map sounds; +}; + +SoundManager g_sound_manager; + +SoundManager::SoundManager(): + impl(std::make_unique()) +{ +} + +ma_sound* +SoundManager::load(const std::string& path) +{ + auto it = impl->sounds.find(path); + if (it == impl->sounds.end()) + { + auto new_it = impl->sounds.insert({path, {}}); + ma_sound* out = &new_it.first->second; + ma_sound_init_from_file(g_mixer.engine(), FS::path(path).c_str(), + MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_ASYNC, nullptr, nullptr, out); + return out; + } else { + return &it->second; + } +} diff --git a/src/audio/sound_manager.hpp b/src/audio/sound_manager.hpp new file mode 100644 index 0000000..508b615 --- /dev/null +++ b/src/audio/sound_manager.hpp @@ -0,0 +1,37 @@ +// SuperTux +// Copyright (C) 2025 MatusGuy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +#ifndef SUPERTUX_SRC_AUDIO_SOUND_MANAGER +#define SUPERTUX_SRC_AUDIO_SOUND_MANAGER + +#include + +#include + +class SoundManager +{ +public: + SoundManager(); + + ma_sound* load(const std::string& path); + +private: + struct Impl; + std::unique_ptr impl; +}; + +extern SoundManager g_sound_manager; + +#endif // SUPERTUX_SRC_AUDIO_SOUND_MANAGER diff --git a/src/types.hpp b/src/types.hpp index 7851f46..cba9a8d 100644 --- a/src/types.hpp +++ b/src/types.hpp @@ -18,6 +18,7 @@ #define HEADER_SUPERTUX_TYPES_HPP #include +#include using i8 = std::int8_t; using i16 = std::int16_t; diff --git a/src/util/filesystem.cpp b/src/util/filesystem.cpp index 70e1439..c95c9c2 100644 --- a/src/util/filesystem.cpp +++ b/src/util/filesystem.cpp @@ -29,7 +29,7 @@ namespace FS std::string path(const std::string& file) { - return std::string(NEOTUX_DATA_DIR) + "/" + file; + return join(NEOTUX_DATA_DIR, file); } std::string @@ -42,4 +42,9 @@ parent_dir(const std::string &file) return file.substr(0, pos); } +std::string join(const std::string &a, const std::string& b) +{ + return a + "/" + b; +} + } diff --git a/src/util/filesystem.hpp b/src/util/filesystem.hpp index 00cec77..6b3a57f 100644 --- a/src/util/filesystem.hpp +++ b/src/util/filesystem.hpp @@ -23,6 +23,7 @@ namespace FS { std::string path(const std::string &file); std::string parent_dir(const std::string &file); + std::string join(const std::string &a, const std::string& b); } #endif diff --git a/tests/game_tests/platforming_test.cpp b/tests/game_tests/platforming_test.cpp index 3b981a7..3f81a62 100644 --- a/tests/game_tests/platforming_test.cpp +++ b/tests/game_tests/platforming_test.cpp @@ -32,7 +32,7 @@ PlatformingTest::run() g_rtcontext.width = winsize.width; g_rtcontext.height = winsize.height; - g_mixer.play_music("music/antarctic/chipdisko.ogg"); + g_mixer.play_music("music/antarctic/chipdisko.music"); g_tiles_reader.open();