mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
lua audio support
This commit is contained in:
@@ -109,7 +109,8 @@ enable_testing()
|
||||
add_executable(cube_script_tests
|
||||
tests/test_cube_script.cpp
|
||||
src/script/cube_script.cpp
|
||||
src/app/audio_player.cpp
|
||||
)
|
||||
target_include_directories(cube_script_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
target_link_libraries(cube_script_tests PRIVATE lua::lua assimp::assimp Bullet::Bullet glm::glm Vorbis::vorbisfile Vorbis::vorbis)
|
||||
target_link_libraries(cube_script_tests PRIVATE sdl::sdl lua::lua assimp::assimp Bullet::Bullet glm::glm Vorbis::vorbisfile Vorbis::vorbis)
|
||||
add_test(NAME cube_script_tests COMMAND cube_script_tests)
|
||||
|
||||
@@ -53,6 +53,14 @@ end
|
||||
|
||||
load_cube_mesh()
|
||||
|
||||
local function init_audio()
|
||||
if type(audio_play_background) == "function" then
|
||||
audio_play_background("modmusic.ogg", true)
|
||||
end
|
||||
end
|
||||
|
||||
init_audio()
|
||||
|
||||
if not cube_mesh_info.loaded then
|
||||
error("Unable to load cube mesh: " .. (cube_mesh_info.error or "unknown"))
|
||||
end
|
||||
@@ -188,6 +196,23 @@ local camera = {
|
||||
far = 10.0,
|
||||
}
|
||||
|
||||
local effect_key = "space"
|
||||
local effect_active = false
|
||||
|
||||
local function update_audio_controls()
|
||||
if type(audio_play_sound) ~= "function" then
|
||||
return
|
||||
end
|
||||
if gui_input.keyStates[effect_key] then
|
||||
if not effect_active then
|
||||
audio_play_sound("modmusic.ogg", false)
|
||||
effect_active = true
|
||||
end
|
||||
else
|
||||
effect_active = false
|
||||
end
|
||||
end
|
||||
|
||||
local zoom_settings = {
|
||||
min_distance = 2.0,
|
||||
max_distance = 12.0,
|
||||
@@ -301,6 +326,7 @@ function get_view_projection(aspect)
|
||||
if gui_input then
|
||||
update_camera_zoom(gui_input.wheel)
|
||||
end
|
||||
update_audio_controls()
|
||||
local view = math3d.look_at(camera.eye, camera.center, camera.up)
|
||||
local projection = math3d.perspective(camera.fov, aspect, camera.near, camera.far)
|
||||
return math3d.multiply(projection, view)
|
||||
|
||||
@@ -7,13 +7,21 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace sdl3cpp::app {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<int16_t> DecodeOgg(const std::filesystem::path& path, int& rate, int& channels) {
|
||||
struct DecodedAudio {
|
||||
std::vector<int16_t> samples;
|
||||
int sampleRate = 0;
|
||||
int channels = 0;
|
||||
};
|
||||
|
||||
DecodedAudio DecodeOgg(const std::filesystem::path& path) {
|
||||
FILE* file = std::fopen(path.string().c_str(), "rb");
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to open audio file: " + path.string());
|
||||
@@ -30,8 +38,8 @@ std::vector<int16_t> DecodeOgg(const std::filesystem::path& path, int& rate, int
|
||||
ov_clear(&oggFile);
|
||||
throw std::runtime_error("Audio metadata is missing");
|
||||
}
|
||||
channels = info->channels;
|
||||
rate = static_cast<int>(info->rate);
|
||||
int channels = info->channels;
|
||||
int rate = static_cast<int>(info->rate);
|
||||
|
||||
std::vector<int16_t> decoded;
|
||||
decoded.reserve(static_cast<size_t>(ov_pcm_total(&oggFile, -1)) * channels);
|
||||
@@ -58,21 +66,96 @@ std::vector<int16_t> DecodeOgg(const std::filesystem::path& path, int& rate, int
|
||||
if (decoded.empty()) {
|
||||
throw std::runtime_error("Decoded audio is empty");
|
||||
}
|
||||
return decoded;
|
||||
|
||||
return DecodedAudio{std::move(decoded), rate, channels};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AudioPlayer::AudioPlayer(const std::filesystem::path& oggPath) {
|
||||
int sampleRate = 0;
|
||||
int channelCount = 0;
|
||||
buffer_ = DecodeOgg(oggPath, sampleRate, channelCount);
|
||||
bufferSizeBytes_ = buffer_.size() * sizeof(int16_t);
|
||||
AudioPlayer::AudioPlayer() = default;
|
||||
|
||||
AudioPlayer::~AudioPlayer() {
|
||||
if (stream_) {
|
||||
SDL_PauseAudioStreamDevice(stream_);
|
||||
SDL_DestroyAudioStream(stream_);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayer::PlayBackground(const std::filesystem::path& path, bool loop) {
|
||||
DecodedAudio clip = DecodeOgg(path);
|
||||
EnsureStream(clip.sampleRate, clip.channels);
|
||||
std::scoped_lock lock(voicesMutex_);
|
||||
backgroundVoice_ = AudioVoice{std::move(clip.samples), 0, loop, true};
|
||||
}
|
||||
|
||||
void AudioPlayer::PlayEffect(const std::filesystem::path& path, bool loop) {
|
||||
DecodedAudio clip = DecodeOgg(path);
|
||||
EnsureStream(clip.sampleRate, clip.channels);
|
||||
std::scoped_lock lock(voicesMutex_);
|
||||
effectVoices_.push_back(AudioVoice{std::move(clip.samples), 0, loop, true});
|
||||
}
|
||||
|
||||
void AudioPlayer::AudioStreamCallback(void* userdata, SDL_AudioStream* stream, int additionalAmount, int totalAmount) {
|
||||
auto* self = static_cast<AudioPlayer*>(userdata);
|
||||
self->FeedStream(stream, totalAmount);
|
||||
}
|
||||
|
||||
void AudioPlayer::FeedStream(SDL_AudioStream* stream, int totalAmount) {
|
||||
if (totalAmount <= 0 || !stream_) {
|
||||
return;
|
||||
}
|
||||
size_t sampleCount = static_cast<size_t>(totalAmount) / sizeof(int16_t);
|
||||
if (sampleCount == 0) {
|
||||
return;
|
||||
}
|
||||
mixBuffer_.assign(sampleCount, 0);
|
||||
|
||||
std::scoped_lock lock(voicesMutex_);
|
||||
if (backgroundVoice_ && backgroundVoice_->active) {
|
||||
AddVoiceSamples(*backgroundVoice_, mixBuffer_, sampleCount);
|
||||
if (!backgroundVoice_->active) {
|
||||
backgroundVoice_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = effectVoices_.begin(); it != effectVoices_.end();) {
|
||||
AddVoiceSamples(*it, mixBuffer_, sampleCount);
|
||||
if (!it->active) {
|
||||
it = effectVoices_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
outputBuffer_.resize(sampleCount);
|
||||
for (size_t i = 0; i < sampleCount; ++i) {
|
||||
int32_t value = mixBuffer_[i];
|
||||
if (value > std::numeric_limits<int16_t>::max()) {
|
||||
value = std::numeric_limits<int16_t>::max();
|
||||
} else if (value < std::numeric_limits<int16_t>::min()) {
|
||||
value = std::numeric_limits<int16_t>::min();
|
||||
}
|
||||
outputBuffer_[i] = static_cast<int16_t>(value);
|
||||
}
|
||||
|
||||
SDL_PutAudioStreamData(stream, outputBuffer_.data(), static_cast<int>(sampleCount * sizeof(int16_t)));
|
||||
}
|
||||
|
||||
void AudioPlayer::EnsureStream(int sampleRate, int channels) {
|
||||
if (sampleRate <= 0 || channels <= 0) {
|
||||
throw std::runtime_error("Audio format is invalid");
|
||||
}
|
||||
if (sampleRate_ != 0 && (sampleRate != sampleRate_ || channels != channels_)) {
|
||||
throw std::runtime_error("Requested audio format does not match initialized stream");
|
||||
}
|
||||
if (stream_) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_AudioSpec desired{};
|
||||
desired.freq = sampleRate;
|
||||
desired.format = SDL_AUDIO_S16;
|
||||
desired.channels = channelCount;
|
||||
desired.channels = channels;
|
||||
|
||||
stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired, &AudioPlayer::AudioStreamCallback, this);
|
||||
if (!stream_) {
|
||||
@@ -83,43 +166,29 @@ AudioPlayer::AudioPlayer(const std::filesystem::path& oggPath) {
|
||||
stream_ = nullptr;
|
||||
throw std::runtime_error("Failed to resume audio stream device: " + std::string(SDL_GetError()));
|
||||
}
|
||||
|
||||
sampleRate_ = sampleRate;
|
||||
channels_ = channels;
|
||||
}
|
||||
|
||||
AudioPlayer::~AudioPlayer() {
|
||||
if (stream_) {
|
||||
SDL_PauseAudioStreamDevice(stream_);
|
||||
SDL_DestroyAudioStream(stream_);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayer::AudioStreamCallback(void* userdata, SDL_AudioStream* stream, int additionalAmount, int totalAmount) {
|
||||
auto* self = static_cast<AudioPlayer*>(userdata);
|
||||
self->FeedStream(stream, totalAmount);
|
||||
}
|
||||
|
||||
void AudioPlayer::FeedStream(SDL_AudioStream* stream, int totalAmount) {
|
||||
if (totalAmount <= 0 || bufferSizeBytes_ == 0) {
|
||||
void AudioPlayer::AddVoiceSamples(AudioVoice& voice, std::vector<int32_t>& mixBuffer, size_t sampleCount) {
|
||||
if (voice.data.empty()) {
|
||||
voice.active = false;
|
||||
return;
|
||||
}
|
||||
const auto* source = reinterpret_cast<const uint8_t*>(buffer_.data());
|
||||
int remaining = totalAmount;
|
||||
while (remaining > 0) {
|
||||
if (positionBytes_ >= bufferSizeBytes_) {
|
||||
positionBytes_ = 0;
|
||||
size_t idx = voice.position;
|
||||
for (size_t sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) {
|
||||
if (idx >= voice.data.size()) {
|
||||
if (voice.loop) {
|
||||
idx = 0;
|
||||
} else {
|
||||
voice.active = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t available = bufferSizeBytes_ - positionBytes_;
|
||||
if (available == 0) {
|
||||
positionBytes_ = 0;
|
||||
continue;
|
||||
}
|
||||
size_t chunk = std::min<size_t>(available, static_cast<size_t>(remaining));
|
||||
int queued = SDL_PutAudioStreamData(stream, source + positionBytes_, static_cast<int>(chunk));
|
||||
if (queued <= 0) {
|
||||
break;
|
||||
}
|
||||
positionBytes_ += static_cast<size_t>(queued);
|
||||
remaining -= queued;
|
||||
mixBuffer[sampleIndex] += static_cast<int32_t>(voice.data[idx++]);
|
||||
}
|
||||
voice.position = idx;
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::app
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
@@ -12,20 +13,38 @@ namespace sdl3cpp::app {
|
||||
|
||||
class AudioPlayer {
|
||||
public:
|
||||
explicit AudioPlayer(const std::filesystem::path& oggPath);
|
||||
AudioPlayer();
|
||||
~AudioPlayer();
|
||||
|
||||
AudioPlayer(const AudioPlayer&) = delete;
|
||||
AudioPlayer& operator=(const AudioPlayer&) = delete;
|
||||
|
||||
void PlayBackground(const std::filesystem::path& path, bool loop = true);
|
||||
void PlayEffect(const std::filesystem::path& path, bool loop = false);
|
||||
|
||||
private:
|
||||
struct AudioVoice {
|
||||
std::vector<int16_t> data;
|
||||
size_t position = 0;
|
||||
bool loop = false;
|
||||
bool active = true;
|
||||
};
|
||||
|
||||
static void AudioStreamCallback(void* userdata, SDL_AudioStream* stream, int additionalAmount, int totalAmount);
|
||||
void FeedStream(SDL_AudioStream* stream, int totalAmount);
|
||||
void EnsureStream(int sampleRate, int channels);
|
||||
void AddVoiceSamples(AudioVoice& voice, std::vector<int32_t>& mixBuffer, size_t sampleCount);
|
||||
|
||||
SDL_AudioStream* stream_ = nullptr;
|
||||
std::vector<int16_t> buffer_;
|
||||
size_t positionBytes_ = 0;
|
||||
size_t bufferSizeBytes_ = 0;
|
||||
int sampleRate_ = 0;
|
||||
int channels_ = 0;
|
||||
|
||||
std::optional<AudioVoice> backgroundVoice_;
|
||||
std::vector<AudioVoice> effectVoices_;
|
||||
|
||||
std::mutex voicesMutex_;
|
||||
std::vector<int32_t> mixBuffer_;
|
||||
std::vector<int16_t> outputBuffer_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::app
|
||||
|
||||
@@ -111,7 +111,8 @@ void Sdl3App::InitSDL() {
|
||||
TRACE_VAR(window_);
|
||||
SDL_StartTextInput(window_);
|
||||
try {
|
||||
audioPlayer_ = std::make_unique<AudioPlayer>(scriptDirectory_ / "modmusic.ogg");
|
||||
audioPlayer_ = std::make_unique<AudioPlayer>();
|
||||
cubeScript_.SetAudioPlayer(audioPlayer_.get());
|
||||
} catch (const std::exception& exc) {
|
||||
std::cerr << "AudioPlayer: " << exc.what() << '\n';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "script/cube_script.hpp"
|
||||
#include "app/audio_player.hpp"
|
||||
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/material.h>
|
||||
@@ -12,6 +13,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -426,11 +428,45 @@ int LuaPhysicsGetTransform(lua_State* L) {
|
||||
lua_rawseti(L, -2, 3);
|
||||
lua_pushnumber(L, orientation.w());
|
||||
lua_rawseti(L, -2, 4);
|
||||
lua_setfield(L, -2, "rotation");
|
||||
lua_setfield(L, -2, "rotation");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LuaAudioPlayBackground(lua_State* L) {
|
||||
auto* script = static_cast<CubeScript*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
bool loop = true;
|
||||
if (lua_gettop(L) >= 2 && lua_isboolean(L, 2)) {
|
||||
loop = lua_toboolean(L, 2);
|
||||
}
|
||||
std::string error;
|
||||
if (!script->QueueAudioCommand(CubeScript::AudioCommandType::Background, path, loop, error)) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LuaAudioPlaySound(lua_State* L) {
|
||||
auto* script = static_cast<CubeScript*>(lua_touserdata(L, lua_upvalueindex(1)));
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
bool loop = false;
|
||||
if (lua_gettop(L) >= 2 && lua_isboolean(L, 2)) {
|
||||
loop = lua_toboolean(L, 2);
|
||||
}
|
||||
std::string error;
|
||||
if (!script->QueueAudioCommand(CubeScript::AudioCommandType::Effect, path, loop, error)) {
|
||||
lua_pushnil(L);
|
||||
lua_pushstring(L, error.c_str());
|
||||
return 2;
|
||||
}
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int LuaGlmMatrixFromTransform(lua_State* L) {
|
||||
std::array<float, 3> translation = detail::ReadVector3(L, 1);
|
||||
std::array<float, 4> rotation = detail::ReadQuaternion(L, 2);
|
||||
@@ -474,6 +510,12 @@ CubeScript::CubeScript(const std::filesystem::path& scriptPath, bool debugEnable
|
||||
lua_pushlightuserdata(L_, this);
|
||||
lua_pushcclosure(L_, &LuaGlmMatrixFromTransform, 1);
|
||||
lua_setglobal(L_, "glm_matrix_from_transform");
|
||||
lua_pushlightuserdata(L_, this);
|
||||
lua_pushcclosure(L_, &LuaAudioPlayBackground, 1);
|
||||
lua_setglobal(L_, "audio_play_background");
|
||||
lua_pushlightuserdata(L_, this);
|
||||
lua_pushcclosure(L_, &LuaAudioPlaySound, 1);
|
||||
lua_setglobal(L_, "audio_play_sound");
|
||||
lua_pushboolean(L_, debugEnabled_);
|
||||
lua_setglobal(L_, "lua_debug");
|
||||
auto scriptDir = scriptPath.parent_path();
|
||||
@@ -969,4 +1011,62 @@ bool CubeScript::ReadStringField(lua_State* L, int index, const char* name, std:
|
||||
return false;
|
||||
}
|
||||
|
||||
void CubeScript::SetAudioPlayer(app::AudioPlayer* audioPlayer) {
|
||||
audioPlayer_ = audioPlayer;
|
||||
if (!audioPlayer_) {
|
||||
return;
|
||||
}
|
||||
for (const auto& command : pendingAudioCommands_) {
|
||||
try {
|
||||
ExecuteAudioCommand(audioPlayer_, command);
|
||||
} catch (const std::exception& exc) {
|
||||
std::cerr << "AudioPlayer: " << exc.what() << '\n';
|
||||
}
|
||||
}
|
||||
pendingAudioCommands_.clear();
|
||||
}
|
||||
|
||||
bool CubeScript::QueueAudioCommand(AudioCommandType type, std::string path, bool loop, std::string& error) {
|
||||
if (audioPlayer_) {
|
||||
try {
|
||||
AudioCommand command{type, std::move(path), loop};
|
||||
ExecuteAudioCommand(audioPlayer_, command);
|
||||
return true;
|
||||
} catch (const std::exception& exc) {
|
||||
error = exc.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
pendingAudioCommands_.push_back(AudioCommand{type, std::move(path), loop});
|
||||
return true;
|
||||
}
|
||||
|
||||
void CubeScript::ExecuteAudioCommand(app::AudioPlayer* player, const AudioCommand& command) {
|
||||
auto resolved = ResolveScriptPath(command.path);
|
||||
if (!std::filesystem::exists(resolved)) {
|
||||
throw std::runtime_error("Audio file not found: " + resolved.string());
|
||||
}
|
||||
switch (command.type) {
|
||||
case AudioCommandType::Background:
|
||||
player->PlayBackground(resolved, command.loop);
|
||||
break;
|
||||
case AudioCommandType::Effect:
|
||||
player->PlayEffect(resolved, command.loop);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path CubeScript::ResolveScriptPath(const std::string& requested) const {
|
||||
std::filesystem::path resolved(requested);
|
||||
if (!resolved.is_absolute()) {
|
||||
resolved = scriptDirectory_ / resolved;
|
||||
}
|
||||
std::error_code ec;
|
||||
auto canonical = std::filesystem::weakly_canonical(resolved, ec);
|
||||
if (!ec) {
|
||||
resolved = canonical;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::script
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
#include "core/vertex.hpp"
|
||||
|
||||
namespace sdl3cpp::app {
|
||||
class AudioPlayer;
|
||||
}
|
||||
|
||||
namespace sdl3cpp::script {
|
||||
|
||||
struct PhysicsBridge;
|
||||
@@ -86,6 +90,11 @@ public:
|
||||
std::string shaderKey = "default";
|
||||
};
|
||||
|
||||
enum class AudioCommandType {
|
||||
Background,
|
||||
Effect,
|
||||
};
|
||||
|
||||
std::vector<SceneObject> LoadSceneObjects();
|
||||
std::array<float, 16> ComputeModelMatrix(int functionRef, float time);
|
||||
std::array<float, 16> GetViewProjectionMatrix(float aspect);
|
||||
@@ -95,8 +104,19 @@ public:
|
||||
bool HasGuiCommands() const;
|
||||
std::filesystem::path GetScriptDirectory() const;
|
||||
PhysicsBridge& GetPhysicsBridge();
|
||||
void SetAudioPlayer(app::AudioPlayer* audioPlayer);
|
||||
bool QueueAudioCommand(AudioCommandType type, std::string path, bool loop, std::string& error);
|
||||
|
||||
private:
|
||||
struct AudioCommand {
|
||||
AudioCommandType type = AudioCommandType::Background;
|
||||
std::string path;
|
||||
bool loop = false;
|
||||
};
|
||||
|
||||
void ExecuteAudioCommand(app::AudioPlayer* player, const AudioCommand& command);
|
||||
std::filesystem::path ResolveScriptPath(const std::string& requested) const;
|
||||
|
||||
static std::vector<core::Vertex> ReadVertexArray(lua_State* L, int index);
|
||||
static std::vector<uint16_t> ReadIndexArray(lua_State* L, int index);
|
||||
static std::string LuaErrorMessage(lua_State* L);
|
||||
@@ -111,6 +131,8 @@ private:
|
||||
std::filesystem::path scriptDirectory_;
|
||||
bool debugEnabled_ = false;
|
||||
std::unique_ptr<PhysicsBridge> physicsBridge_;
|
||||
app::AudioPlayer* audioPlayer_ = nullptr;
|
||||
std::vector<AudioCommand> pendingAudioCommands_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::script
|
||||
|
||||
Reference in New Issue
Block a user