lua audio support

This commit is contained in:
Richard Ward
2025-12-19 17:21:23 +00:00
parent d690516c2c
commit 8f434d8fd0
7 changed files with 287 additions and 49 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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