diff --git a/src/Audio.cpp b/src/Audio.cpp index 209987e..43cefbd 100644 --- a/src/Audio.cpp +++ b/src/Audio.cpp @@ -1,102 +1,138 @@ #include "Audio.hpp" -BGM::BGM(Node* parent) : Node(parent) {} +using namespace sb::audio; -BGM::BGM() : BGM(nullptr) {} - -BGM::BGM(Node* parent, const fs::path& path) : BGM(parent) +Chunk::Chunk(const fs::path& path) { - load_music(path); + load(path); } -BGM::BGM(const fs::path& path) : BGM(nullptr, path) {} - -void BGM::load_music(const fs::path& path) +void Chunk::load(const fs::path& path) { - music = Mix_LoadMUS(path.c_str()); + chunk = std::shared_ptr(Mix_LoadWAV(path.c_str()), Mix_FreeChunk); + if (chunk.get() == nullptr) + { + std::ostringstream message; + message << "Unable to load audio chunk at " << path << "."; + Log::sdl_error(message.str(), Log::Level::WARN); + } } -void BGM::set_volume(float v) +std::uint8_t Chunk::convert_volume(float volume) { - volume = v; + return std::clamp(static_cast(std::round(volume * MIX_MAX_VOLUME)), 0, MIX_MAX_VOLUME); } -void BGM::play(int loops) +float Chunk::volume() const { - Mix_VolumeMusic(std::round(volume * 127)); - Mix_PlayMusic(music, loops); + std::uint8_t sdl_volume = Mix_VolumeChunk(chunk.get(), -1); + return static_cast(sdl_volume) / static_cast(MIX_MAX_VOLUME); } -BGM::~BGM() +void Chunk::volume(float level) { - Mix_FreeMusic(music); + Mix_VolumeChunk(chunk.get(), convert_volume(level)); } -SoundEffect::SoundEffect(Node* parent) : Node(parent) {} - -SoundEffect::SoundEffect() : SoundEffect(nullptr) {} - -SoundEffect::SoundEffect(Node* parent, const fs::path& path) : SoundEffect(parent) +void Chunk::channel_volume(float volume) { - load_chunk(path); + int count = Mix_GroupCount(-1); + for (int channel = 0; channel < count; channel++) + { + if (Mix_GetChunk(channel) == chunk.get()) + { + Mix_Volume(channel, convert_volume(volume)); + } + } } -SoundEffect::SoundEffect(const fs::path& path) : SoundEffect(nullptr, path) {} - -void SoundEffect::load_chunk(const fs::path& path) -{ - chunk = Mix_LoadWAV(path.c_str()); - Mix_VolumeChunk(chunk, std::round(volume * 127)); -} - -void SoundEffect::play(float location) -{ - play(); - Box window = window_box(); - location = std::clamp(location, window.left(), window.right()); - float location_relative = (location - window.left()) / window.width(); - int angle = 270 - location_relative * 180; - Mix_SetPosition(channel, angle, 0); -} - -void SoundEffect::play() -{ - channel = Mix_PlayChannel(-1, chunk, loops); -} - -void SoundEffect::play_after(float delay) -{ - play_animation.play_once(delay); -} - -void SoundEffect::set_volume(float v) -{ - volume = v; - Mix_VolumeChunk(chunk, std::round(volume * 127)); -} - -void SoundEffect::set_loop_count(int count) -{ - loops = count; -} - -void SoundEffect::set_loop_forever() +void Chunk::loop() { loops = -1; } -bool SoundEffect::playing(bool include_delay) const +void Chunk::loop(int count) { - if (include_delay) + loops = count; +} + +int Chunk::play(float fade, int channel) +{ + /* Play the audio with a fade in time if any was specified. */ + int assignment; + if (fade <= 0.0f) { - if (play_animation.playing()) + assignment = Mix_PlayChannel(channel, chunk.get(), loops); + } + else + { + int milliseconds = static_cast(std::round(fade * 1000.0f)); + assignment = Mix_FadeInChannel(channel, chunk.get(), loops, milliseconds); + } + + /* Check if the audio is paused on other channels. If so, stop the audio on those channels. */ + int count = Mix_GroupCount(-1); + for (int index = 0; index < count; index++) + { + if (index != assignment && Mix_GetChunk(index) == chunk.get() && Mix_Paused(index)) { - return true; + Mix_HaltChannel(index); } } - for (int channel_ii = 0; channel_ii < Mix_GroupAvailable(-1); channel_ii++) + + return assignment; +} + +void Chunk::stop(float fade) +{ + int count = Mix_GroupCount(-1); + for (int channel = 0; channel < count; channel++) { - if (Mix_GetChunk(channel_ii) == chunk) + if (Mix_GetChunk(channel) == chunk.get()) + { + if (fade <= 0.0f) + { + Mix_HaltChannel(channel); + } + else + { + int milliseconds = static_cast(std::round(fade * 1000.0f)); + Mix_FadeOutChannel(channel, milliseconds); + } + } + } +} + +void Chunk::pause() +{ + int count = Mix_GroupCount(-1); + for (int channel = 0; channel < count; channel++) + { + if (Mix_GetChunk(channel) == chunk.get()) + { + Mix_Pause(channel); + } + } +} + +void Chunk::resume() +{ + int count = Mix_GroupCount(-1); + for (int channel = 0; channel < count; channel++) + { + if (Mix_GetChunk(channel) == chunk.get()) + { + Mix_Resume(channel); + } + } +} + +bool Chunk::playing() const +{ + int count = Mix_GroupCount(-1); + for (int channel = 0; channel < count; channel++) + { + if (Mix_GetChunk(channel) == chunk.get() && Mix_Playing(channel)) { return true; } @@ -104,32 +140,47 @@ bool SoundEffect::playing(bool include_delay) const return false; } -void SoundEffect::update(float timestamp) +bool Chunk::paused() const { - play_animation.update(timestamp); -} - -SoundEffect::~SoundEffect() -{ - Mix_FreeChunk(chunk); -} - -Audio::Audio(Node* parent) : Node(parent) {} - -void Audio::load_sfx() -{ - load_directory(configuration()["audio"]["default-sfx-root"], sfx, this); -} - -void Audio::load_bgm() -{ - load_directory(configuration()["audio"]["default-bgm-root"], bgm, this); -} - -void Audio::update(float timestamp) -{ - for (auto& [name, sound_effect] : sfx) + int count = Mix_GroupCount(-1); + for (int channel = 0; channel < count; channel++) { - sound_effect.update(timestamp); + if (Mix_GetChunk(channel) == chunk.get() && Mix_Paused(channel)) + { + return true; + } + } + return false; +} + +bool Chunk::fading() const +{ + int count = Mix_GroupCount(-1); + for (int channel = 0; channel < count; channel++) + { + if (Mix_GetChunk(channel) == chunk.get() && Mix_FadingChannel(channel)) + { + return true; + } + } + return false; +} + +Music::Music(const fs::path& path) +{ + load(path); +} + +void Music::load(const fs::path& path) +{ + music = std::shared_ptr(Mix_LoadMUS(path.c_str()), Mix_FreeMusic); + if (music.get() == nullptr) + { + Log::sdl_error(); } } + +void Music::play(int loops) +{ + Mix_PlayMusic(music.get(), loops); +} diff --git a/src/Audio.hpp b/src/Audio.hpp index 5e1a43f..10c1f62 100644 --- a/src/Audio.hpp +++ b/src/Audio.hpp @@ -13,91 +13,163 @@ #include #include #include +#include #include "SDL.h" #include "SDL_mixer.h" #include "filesystem.hpp" -#include "Node.hpp" -#include "Animation.hpp" -#include "extension.hpp" +#include "Log.hpp" -struct BGM : Node +namespace sb::audio { - Mix_Music* music; - float volume = 1.0f; - - BGM(); - BGM(Node*); - BGM(Node*, const fs::path&); - BGM(const fs::path&); - void load_music(const fs::path&); - void set_volume(float); - void play(int = -1); - ~BGM(); - -}; - -struct SoundEffect : Node -{ - - Mix_Chunk* chunk = nullptr; - int channel, loops = 0; - float volume = 1.0f; - - /* The cast is required so that the overloaded play function without arguments will be selected. */ - Animation play_animation = Animation(std::bind(static_cast(&SoundEffect::play), this)); - - SoundEffect(Node*); - SoundEffect(); - SoundEffect(Node*, const fs::path&); - SoundEffect(const fs::path&); - void load_chunk(const fs::path&); - void play(float); - void play(); - void play_after(float); - void set_volume(float); - void set_loop_count(int); - void set_loop_forever(); - bool playing(bool = false) const; - void update(float timestamp); - ~SoundEffect(); - -}; - -struct Audio : Node -{ - - std::map sfx; - std::map bgm; - - Audio(Node*); - void load_sfx(); - void load_bgm(); - void update(float timestamp); - - template - void load_directory(const fs::path& root, std::map& storage, Node* parent = nullptr) + /*! + * Load audio from an OGG or WAV file and play it on multiple channels. + * + * This class interfaces with SDL mixer's API and its `Mix_Chunk` audio data struct. + * + * There are some differences between how `Mix_Chunk` and `Mix_Music` can be used with the API, most notably that multiple chunks can be playing on + * multiple channels simultaneously, but only one music object can be playing at a time. This is most likely because `Mix_Music` objects are streamed + * and `Mix_Chunk` are loaded into memory, but I haven't verified that by looking at the internal code. + */ + class Chunk { - SDL_Log("looking for audio files in %s", root.c_str()); - if (fs::exists(root)) - { - for (const fs::path& path : sb::glob(root / ".*")) - { - std::regex pattern("([^.]+).*$"); - std::smatch match; - std::string basename = path.filename(); - if (std::regex_match(basename, match, pattern)) - { - std::string key = match[1].str(); - SDL_Log("loading %s as %s", path.c_str(), key.c_str()); - storage.try_emplace(key, parent, path); - } - } - } - } + private: + + std::shared_ptr chunk; + + /* -1 means loop forever, any other value is the number of times to loop */ + int loops = 0; + + /*! + * Convert floating point volume between 0.0 and 1.0 to SDL 8-bit unsigned integer volume between 0 and `MIX_MAX_VOLUME`. + * + * @param volume Volume between 0.0 and 1.0 + */ + static std::uint8_t convert_volume(float volume); + + public: + + /*! + * Create an empty audio chunk. + */ + Chunk() = default; + + /*! + * Create an audio chunk and load it with data from an OGG or WAV file at the given path. If the given path does not contain loadable data, + * a warning will be printed, and an empty chunk will be created. + * + * This will call Chunk::load(const fs::path&) automatically. + * + * @param path path to an OGG or WAV file + */ + Chunk(const fs::path& path); + + /*! + * Load audio data from an OGG or WAV file at the given path, replacing any existing audio data in this object. If the given path does not contain + * loadable data, a warning will be printed, and existing audio data will not be overwritten. + * + * @param path path to an OGG or WAV file + */ + void load(const fs::path& path); + + /*! + * @return the volume of the audio chunk in the range 0.0 - 1.0 + */ + float volume() const; + + /*! + * @param level set the volume of the chunk to a value between 0.0 and 1.0 + */ + void volume(float level); + + /*! + * Set the volume of all channels currently playing the audio chunk to a given level. This does not modify the volume of the chunk itself. + * + * @param level Volume between 0.0 and 1.0 + */ + void channel_volume(float level); + + /*! + * Set audio to loop forever. + */ + void loop(); + + /*! + * @param count Number of times audio should loop when played + */ + void loop(int count); + + /*! + * Play the audio data loaded into this object once on the first available free channel. Optionally fade in playback over a given amount of time + * in seconds. + * + * This always plays the audio from the beginning even if the audio is paused. To resume instead, use Chunk::resume(). Any audio chunks currently + * paused will be stopped. + * + * If the audio is playing already, however, it will continue to play on that channel, and it will be played again from the beginning on another + * channel. + * + * @param fade Length of fade in in seconds + * @param channel The SDL mixer channel to play the sound on. A value of -1 means choose a channel automatically. + * @return The channel assigned to the audio chunk + */ + int play(float fade = 0.0f, int channel = -1); + + /*! + * Stop playback of this audio chunk on all channels it is playing on. Optionally fade out over the given amount of time in seconds. + * + * @param fade length of fade out in seconds + */ + void stop(float fade = 0.0f); + + /*! + * Pause audio chunk on all channels it is playing on. + */ + void pause(); + + /*! + * Resume audio chunk on all channels it is playing on. + */ + void resume(); + + /*! + * @return True if the audio is playing on any channel, false otherwise. Note that paused or fading audio is considered playing. + */ + bool playing() const; + + /*! + * @return True if the audio is paused on any channels, false otherwise. + */ + bool paused() const; + + /*! + * @return True if the audio is fading in or out on any channels, false otherwise. + */ + bool fading() const; + }; + + /*! + * Load audio from an OGG or WAV file and play it on a single channel dedicated to music. Only one music object can be playing at a time. + * + * This class interfaces with SDL mixer's API and Mix_Music audio data pointer. + * + * There are some differences between how Mix_Chunk and Mix_Music can be used with the API, most notably that multiple chunks can be playing on + * multiple channels simultaneously, but only one music object can be playing at a time. This is most likely because Mix_Music objects are streamed + * and Mix_Chunk are loaded into memory, but I haven't verified that by looking at the internal code. + */ + class Music + { + + private: + + std::shared_ptr music; + + public: + + Music() = default; + Music(const fs::path& path); + void load(const fs::path& path); + void play(int loops = -1); + }; }; - -#include "Box.hpp" -#include "Configuration.hpp" -#include "Display.hpp" diff --git a/src/Game.cpp b/src/Game.cpp index a13c592..7290752 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -187,9 +187,8 @@ Game::Game() { SDL_Log("initialized SDL mixer %d.%d.%d", SDL_MIXER_MAJOR_VERSION, SDL_MIXER_MINOR_VERSION, SDL_MIXER_PATCHLEVEL); } - // if (Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT, - // MIX_DEFAULT_CHANNELS, 1024) < 0) - if (Mix_OpenAudio(11025, AUDIO_U8, MIX_DEFAULT_CHANNELS, 2048) < 0) + if (Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS, 1024) < 0) + // if (Mix_OpenAudio(11025, AUDIO_U8, MIX_DEFAULT_CHANNELS, 2048) < 0) { sb::Log::sdl_error("Could not set up audio"); } @@ -201,8 +200,6 @@ Game::Game() message << "Found audio capture device " << ii << ": " << SDL_GetAudioDeviceName(ii, SDL_TRUE); sb::Log::log(message); } - audio.load_sfx(); - audio.load_bgm(); #if SDL_BYTEORDER == SDL_BIG_ENDIAN sb::Log::log("big endian"); #else @@ -539,11 +536,6 @@ Input& Game::get_input() return input; } -Audio& Game::get_audio() -{ - return audio; -} - std::shared_ptr sb::Game::font() const { return _font; @@ -621,7 +613,6 @@ void Game::frame(float timestamp) float timestamp_seconds = timestamp / 1000.0f; // recorder.update(timestamp_seconds); _delegate.dispatch(); - audio.update(timestamp_seconds); input.unsuppress_animation.update(timestamp_seconds); update(timestamp_seconds); _configuration.update(timestamp_seconds); @@ -676,7 +667,6 @@ void Game::handle_quit_event(SDL_Event &event) void Game::quit() { /* Unsubscribe all delegate object's subscribers. */ - _delegate.unsubscribe(&audio); _delegate.unsubscribe(&display); _delegate.unsubscribe(&input); _delegate.unsubscribe(this); diff --git a/src/Game.hpp b/src/Game.hpp index 22455fa..9521aa6 100644 --- a/src/Game.hpp +++ b/src/Game.hpp @@ -110,7 +110,6 @@ public: sb::Display display {this}; // Recorder recorder {this}; Input input {this}; - Audio audio {this}; std::vector frame_length_history; Game(); @@ -144,7 +143,6 @@ public: SDL_Renderer* get_renderer(); const Input& get_input() const; Input& get_input(); - Audio& get_audio(); /*! * @return shared pointer to the default font pre-loaded at construction diff --git a/src/Log.cpp b/src/Log.cpp index 16a7422..6d92ffe 100644 --- a/src/Log.cpp +++ b/src/Log.cpp @@ -72,10 +72,10 @@ bool sb::Log::gl_errors(const std::string& heading) return error_logged; } -std::ostringstream sb::Log::sdl_error(const std::string& message) +std::ostringstream sb::Log::sdl_error(const std::string& message, const Level level) { std::ostringstream full_message; full_message << message << " " << SDL_GetError(); - log(full_message, Level::ERROR); + log(full_message, level); return full_message; } diff --git a/src/Log.hpp b/src/Log.hpp index 9696f21..4eb8179 100644 --- a/src/Log.hpp +++ b/src/Log.hpp @@ -80,13 +80,15 @@ namespace sb static bool gl_errors(const std::string& heading = ""); /*! - * Log a message, adding the results of `SDL_GetError` to the end of the message. Should be used to add more information to - * an error statement when the error happened in SDL. The priority level will always be `Level::ERROR`. + * Log a message, adding the results of `SDL_GetError` to the end of the message. This should be used to log a message after an + * SDL API function returns an error value because SDL functions are written to set the value of SDL_GetError in that case. The + * priority level will be `Level::ERROR` unless a different level is specified. * * @param message text to log before the SDL error is appended + * @param level message priority level * @return a string stream containing the full message */ - static std::ostringstream sdl_error(const std::string& message = ""); + static std::ostringstream sdl_error(const std::string& message = "", const Level level = ERROR); }; diff --git a/src/Model.hpp b/src/Model.hpp index 32b58c8..edf0418 100644 --- a/src/Model.hpp +++ b/src/Model.hpp @@ -180,23 +180,22 @@ namespace sb std::vector& textures(); /*! - * Get a constant reference to the texture at the given index. If no index is given, get the texture at index 0. If - * there are no textures, an exception will be thrown. + * Get a constant reference to the texture at the given index. If there are no textures, an exception will be thrown. * * @param index index of texture to get */ - const sb::Texture& texture(int index = 0) const; + const sb::Texture& texture(int index) const; /*! - * Get the texture at the given index. If no index is given, get the texture at index 0. If there are no textures, - * an exception will be thrown. + * Get the texture at the given index. If there are no textures, an exception will be thrown. * * @param index index of texture to get */ - sb::Texture& texture(int index = 0); + sb::Texture& texture(int index); /*! - * Add a copy of the given texture to model's list of textures. + * Add a copy of the given texture to model's list of textures. Note that the texture object is copied, but the texture data is not copied. + * The texture data is stored as a shared pointer in the texture object, so only the pointer is copied. * * @param texture texture to add to model */ diff --git a/src/Pad.hpp b/src/Pad.hpp index 863b5b8..66f488b 100644 --- a/src/Pad.hpp +++ b/src/Pad.hpp @@ -50,6 +50,7 @@ namespace sb sb::Switch connection; sb::Plane _plane; Box box; + int texture_index = 0; public: @@ -221,7 +222,18 @@ namespace sb { glUniform1i(texture_flag_uniform.value(), true); } - _plane.texture().bind(); + + /* Determine texture index by checking the state of the pad and the amount of available textures. If there is more than 1 texture, + * the texture will correspond with the state. */ + if (connection && _plane.textures().size() > 1) + { + texture_index = 1; + } + else + { + texture_index = 0; + } + _plane.texture(texture_index).bind(); } else if (texture_flag_uniform.has_value()) { @@ -245,6 +257,24 @@ namespace sb return connection.flip(args...); } + /*! + * @return the state of the pad + */ + bool pressed() const + { + return connection; + } + + /*! + * This will not trigger the callback if any is set. To do that, use Pad::press(Arguments...). + * + * @param state true or false to set the pad state + */ + void state(bool pressed) + { + connection = pressed; + } + /*! * @return size in bytes of the pad object's plane object */ diff --git a/src/Sprite.hpp b/src/Sprite.hpp index 0241817..f71972f 100644 --- a/src/Sprite.hpp +++ b/src/Sprite.hpp @@ -19,7 +19,7 @@ namespace sb { /*! - * A Sprite is a container for an sb::Plane object that resets and stores scale, translation, and rotation matrices every time they are set + * A Sprite is a wrapper around an sb::Plane object that resets and stores scale, translation, and rotation matrices every time they are set * and combines them automatically when getting the full transformation. This allows those transformations to be set repeatedly without having * to call sb::Plane::untransform() after each set. In the case of translation, there is also a move function that allows the translation to be * modified without resetting, so the sprite can move relative amounts. @@ -44,8 +44,8 @@ namespace sb public: /*! - * Construct an instance of Sprite using an existing plane object. The plane will be copied into the Sprite, so further edits will - * need to go through the Sprite class. + * Construct an instance of Sprite using an existing plane object. The plane will be copied into the Sprite, so further edits must + * be made using the Sprite class. * * @param plane flat model of the sprite */ @@ -63,7 +63,7 @@ namespace sb /*! * Construct a Sprite with a default constructed sb::Plane and attach a list of textures to the plane. Each texture is a frame of the - * sprite's animation. The texture is the 2D graphic that displays in the sprite object's location. + * sprite's animation. The texture is the 2D graphic drawn at the sprite's location. * * @param textures list of textures * @param scale amount to scale @@ -77,8 +77,8 @@ namespace sb } /*! - * Construct a Sprite with a default constructed sb::Plane and give the plane a texture. The texture is the 2D graphic that displays - * in the sprite object's location. + * Construct a Sprite with a default constructed sb::Plane and give the plane a texture. The texture is the 2D graphic drawn at the + * sprite's location. * * @param texture sprite's 2D graphic * @param scale amount to scale @@ -128,7 +128,7 @@ namespace sb } /*! - * Add a new texture to the sprite's plane object from a path to an image file which will converted into a new texture. + * Add a new texture to the sprite's plane object from a path to an image file which will be converted into a new texture. * * The texture is loaded into GPU memory if the GL context is active. Otherwise, the path is just attached to the texture, * and it must be loaded with a call to Sprite::load. @@ -167,12 +167,10 @@ namespace sb /*! * @param index set the object's texture index - * @return constant reference to the texture at the given index */ - const sb::Texture& texture_index(int index) + void texture_index(int index) { _texture_index = index; - return texture(); } /*! diff --git a/src/Text.cpp b/src/Text.cpp index 1001aed..d8e2868 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -107,8 +107,8 @@ void Text::refresh() /* Generate texture and create storage. Load pixels from the text rendering surface into the texture. The texture object will handle * destroying the previous texture. The texture object is optimized so that it won't reallocate pixel memory if it was previously * generated at the same size. */ - texture().generate({flipped->w, flipped->h}, GL_RGBA8, _scaling_quality); - texture().load(flipped.get()); + texture(0).generate({flipped->w, flipped->h}, GL_RGBA8, _scaling_quality); + texture(0).load(flipped.get()); } } } diff --git a/src/Texture.hpp b/src/Texture.hpp index 795542f..69a5e7a 100644 --- a/src/Texture.hpp +++ b/src/Texture.hpp @@ -157,11 +157,11 @@ namespace sb * generated, using Texture::generate() or Texture::generate(glm::vec2, GLenum, GLint), otherwise an exception will be * thrown. * - * From the OpenGL manual: - * * > While a texture is bound, GL operations on the target to which it is bound affect the bound texture, and queries of * > the target to which it is bound return state from the bound texture. In effect, the texture targets become aliases for * > the textures currently bound to them. + * > + * > - OpenGL Manual */ void bind() const override;