- redesign audio library to use Chunk and Music classes which align more closely with SDL Mix_Chunk and Mix_Music

- remove default texture index from Model class
- add support for two textures to Pad class
- restore SDL audio mixer initialization parameters to SDL defaults
- add optional log level to SDL error logging function
This commit is contained in:
ohsqueezy 2023-10-09 23:50:18 -04:00
parent 5046b4bcf1
commit d575307b15
11 changed files with 353 additions and 213 deletions

View File

@ -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_Chunk>(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<int>(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<float>(sdl_volume) / static_cast<float>(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<int>(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<int>(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_Music>(Mix_LoadMUS(path.c_str()), Mix_FreeMusic);
if (music.get() == nullptr)
{
Log::sdl_error();
}
}
void Music::play(int loops)
{
Mix_PlayMusic(music.get(), loops);
}

View File

@ -13,91 +13,163 @@
#include <regex>
#include <map>
#include <functional>
#include <algorithm>
#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<void(SoundEffect::*)()>(&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<std::string, SoundEffect> sfx;
std::map<std::string, BGM> bgm;
Audio(Node*);
void load_sfx();
void load_bgm();
void update(float timestamp);
template <typename T>
void load_directory(const fs::path& root, std::map<std::string, T>& 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<Mix_Chunk> 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<Mix_Music> 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"

View File

@ -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<TTF_Font> 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);

View File

@ -110,7 +110,6 @@ public:
sb::Display display {this};
// Recorder recorder {this};
Input input {this};
Audio audio {this};
std::vector<float> 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

View File

@ -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;
}

View File

@ -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);
};

View File

@ -180,23 +180,22 @@ namespace sb
std::vector<sb::Texture>& 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
*/

View File

@ -50,6 +50,7 @@ namespace sb
sb::Switch<ReturnType, Arguments...> 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
*/

View File

@ -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();
}
/*!

View File

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

View File

@ -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;