sound work

This commit is contained in:
Richard Ward
2025-12-19 17:11:38 +00:00
parent 9ce36f3839
commit d690516c2c
10 changed files with 316 additions and 78 deletions

View File

@@ -78,12 +78,14 @@ find_package(CLI11 CONFIG REQUIRED)
find_package(rapidjson CONFIG REQUIRED)
find_package(assimp CONFIG REQUIRED)
find_package(Bullet CONFIG REQUIRED)
find_package(Vorbis CONFIG REQUIRED)
find_package(glm CONFIG REQUIRED)
if(BUILD_SDL3_APP)
add_executable(sdl3_app
src/main.cpp
src/app/sdl3_app_core.cpp
src/app/audio_player.cpp
src/app/sdl3_app_device.cpp
src/app/sdl3_app_swapchain.cpp
src/app/sdl3_app_pipeline.cpp
@@ -95,7 +97,7 @@ add_executable(sdl3_app
src/script/cube_script.cpp
)
target_include_directories(sdl3_app PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(sdl3_app PRIVATE sdl::sdl Vulkan::Vulkan lua::lua CLI11::CLI11 rapidjson assimp::assimp Bullet::Bullet glm::glm)
target_link_libraries(sdl3_app PRIVATE sdl::sdl Vulkan::Vulkan lua::lua CLI11::CLI11 rapidjson assimp::assimp Bullet::Bullet glm::glm Vorbis::vorbisfile Vorbis::vorbis)
target_compile_definitions(sdl3_app PRIVATE SDL_MAIN_HANDLED)
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/shaders" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
@@ -109,5 +111,5 @@ add_executable(cube_script_tests
src/script/cube_script.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)
target_link_libraries(cube_script_tests PRIVATE lua::lua assimp::assimp Bullet::Bullet glm::glm Vorbis::vorbisfile Vorbis::vorbis)
add_test(NAME cube_script_tests COMMAND cube_script_tests)

View File

@@ -28,3 +28,4 @@ class SDL3CppConan(ConanFile):
self.requires("box2d/3.1.1")
self.requires("assimp/6.0.2")
self.requires("glm/1.0.1")
self.requires("vorbis/1.3.7")

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Convert the bundled XM tracker file to an OGG so the demo can play music."""
from __future__ import annotations
import argparse
import shlex
import subprocess
from pathlib import Path
import imageio_ffmpeg
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Convert scripts/modmusic.xm into OGG.")
parser.add_argument(
"--input",
type=Path,
default=Path(__file__).parent / "modmusic.xm",
help="Tracker file to render (default: scripts/modmusic.xm).",
)
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).parent / "modmusic.ogg",
help="Path for the rendered OGG (default next to scripts/modmusic.xm).",
)
parser.add_argument(
"--bitrate",
default="192k",
help="FFmpeg audio bitrate (default: 192k).",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
if not args.input.exists():
raise SystemExit(f"Error: XM source {args.input} is missing")
args.output.parent.mkdir(parents=True, exist_ok=True)
ffmpeg_path = imageio_ffmpeg.get_ffmpeg_exe()
ffmpeg_cmd = [
ffmpeg_path,
"-y",
"-i",
str(args.input),
"-b:a",
args.bitrate,
str(args.output),
]
print("Executing:", " ".join(shlex.quote(arg) for arg in ffmpeg_cmd))
subprocess.run(ffmpeg_cmd, check=True)
if __name__ == "__main__":
main()

BIN
scripts/modmusic.ogg Normal file

Binary file not shown.

125
src/app/audio_player.cpp Normal file
View File

@@ -0,0 +1,125 @@
#include "app/audio_player.hpp"
#include <SDL3/SDL.h>
#include <vorbis/vorbisfile.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <stdexcept>
namespace sdl3cpp::app {
namespace {
std::vector<int16_t> DecodeOgg(const std::filesystem::path& path, int& rate, int& channels) {
FILE* file = std::fopen(path.string().c_str(), "rb");
if (!file) {
throw std::runtime_error("Failed to open audio file: " + path.string());
}
OggVorbis_File oggFile{};
if (ov_open(file, &oggFile, nullptr, 0) < 0) {
std::fclose(file);
throw std::runtime_error("Failed to open OGG stream: " + path.string());
}
vorbis_info* info = ov_info(&oggFile, -1);
if (!info) {
ov_clear(&oggFile);
throw std::runtime_error("Audio metadata is missing");
}
channels = info->channels;
rate = static_cast<int>(info->rate);
std::vector<int16_t> decoded;
decoded.reserve(static_cast<size_t>(ov_pcm_total(&oggFile, -1)) * channels);
int bitstream = 0;
constexpr size_t kChunkBytes = 4096 * sizeof(int16_t);
std::vector<char> chunk(kChunkBytes);
while (true) {
long bytesRead = ov_read(&oggFile, chunk.data(), static_cast<int>(chunk.size()), 0, 2, 1, &bitstream);
if (bytesRead == 0) {
break;
}
if (bytesRead < 0) {
ov_clear(&oggFile);
throw std::runtime_error("Error decoding OGG stream");
}
size_t samples = static_cast<size_t>(bytesRead) / sizeof(int16_t);
size_t oldSize = decoded.size();
decoded.resize(oldSize + samples);
std::memcpy(decoded.data() + oldSize, chunk.data(), samples * sizeof(int16_t));
}
ov_clear(&oggFile);
if (decoded.empty()) {
throw std::runtime_error("Decoded audio is empty");
}
return decoded;
}
} // 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);
SDL_AudioSpec desired{};
desired.freq = sampleRate;
desired.format = SDL_AUDIO_S16;
desired.channels = channelCount;
stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired, &AudioPlayer::AudioStreamCallback, this);
if (!stream_) {
throw std::runtime_error("Failed to open audio stream: " + std::string(SDL_GetError()));
}
if (!SDL_ResumeAudioStreamDevice(stream_)) {
SDL_DestroyAudioStream(stream_);
stream_ = nullptr;
throw std::runtime_error("Failed to resume audio stream device: " + std::string(SDL_GetError()));
}
}
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) {
return;
}
const auto* source = reinterpret_cast<const uint8_t*>(buffer_.data());
int remaining = totalAmount;
while (remaining > 0) {
if (positionBytes_ >= bufferSizeBytes_) {
positionBytes_ = 0;
}
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;
}
}
} // namespace sdl3cpp::app

33
src/app/audio_player.hpp Normal file
View File

@@ -0,0 +1,33 @@
#ifndef SDL3CPP_APP_AUDIO_PLAYER_HPP
#define SDL3CPP_APP_AUDIO_PLAYER_HPP
#include <cstdint>
#include <filesystem>
#include <memory>
#include <vector>
#include <SDL3/SDL.h>
namespace sdl3cpp::app {
class AudioPlayer {
public:
explicit AudioPlayer(const std::filesystem::path& oggPath);
~AudioPlayer();
AudioPlayer(const AudioPlayer&) = delete;
AudioPlayer& operator=(const AudioPlayer&) = delete;
private:
static void AudioStreamCallback(void* userdata, SDL_AudioStream* stream, int additionalAmount, int totalAmount);
void FeedStream(SDL_AudioStream* stream, int totalAmount);
SDL_AudioStream* stream_ = nullptr;
std::vector<int16_t> buffer_;
size_t positionBytes_ = 0;
size_t bufferSizeBytes_ = 0;
};
} // namespace sdl3cpp::app
#endif // SDL3CPP_APP_AUDIO_PLAYER_HPP

View File

@@ -18,10 +18,15 @@
#include <SDL3/SDL_vulkan.h>
#include <vulkan/vulkan.h>
#include "app/audio_player.hpp"
#include "core/vertex.hpp"
#include "script/cube_script.hpp"
#include "gui/gui_renderer.hpp"
namespace sdl3cpp::app {
class AudioPlayer;
}
namespace sdl3cpp::app {
namespace script = sdl3cpp::script;
@@ -135,6 +140,8 @@ private:
std::unique_ptr<gui::GuiRenderer> guiRenderer_;
bool guiHasCommands_ = false;
std::vector<RenderObject> renderObjects_;
std::filesystem::path scriptDirectory_;
std::unique_ptr<AudioPlayer> audioPlayer_;
};
} // namespace sdl3cpp::app

View File

@@ -1,8 +1,10 @@
#include "app/audio_player.hpp"
#include "app/sdl3_app.hpp"
#include "app/trace.hpp"
#include <chrono>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
@@ -82,7 +84,8 @@ void ThrowSdlErrorIfFailed(bool success, const char* context) {
} // namespace
Sdl3App::Sdl3App(const std::filesystem::path& scriptPath, bool luaDebug)
: cubeScript_(scriptPath, luaDebug) {
: cubeScript_(scriptPath, luaDebug),
scriptDirectory_(scriptPath.parent_path()) {
TRACE_FUNCTION();
TRACE_VAR(scriptPath);
}
@@ -99,7 +102,7 @@ void Sdl3App::InitSDL() {
TRACE_FUNCTION();
TRACE_VAR(kWidth);
TRACE_VAR(kHeight);
ThrowSdlErrorIfFailed(SDL_Init(SDL_INIT_VIDEO), "SDL_Init failed");
ThrowSdlErrorIfFailed(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO), "SDL_Init failed");
ThrowSdlErrorIfFailed(SDL_Vulkan_LoadLibrary(nullptr), "SDL_Vulkan_LoadLibrary failed");
window_ = SDL_CreateWindow("SDL3 Vulkan Demo", kWidth, kHeight, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);
if (!window_) {
@@ -107,6 +110,11 @@ void Sdl3App::InitSDL() {
}
TRACE_VAR(window_);
SDL_StartTextInput(window_);
try {
audioPlayer_ = std::make_unique<AudioPlayer>(scriptDirectory_ / "modmusic.ogg");
} catch (const std::exception& exc) {
std::cerr << "AudioPlayer: " << exc.what() << '\n';
}
}
void Sdl3App::InitVulkan() {
@@ -188,6 +196,7 @@ void Sdl3App::Cleanup() {
}
SDL_Vulkan_UnloadLibrary();
SDL_StopTextInput(window_);
audioPlayer_.reset();
SDL_Quit();
}

View File

@@ -22,8 +22,6 @@
namespace sdl3cpp::script {
namespace {
struct PhysicsBridge {
struct BodyRecord {
std::unique_ptr<btCollisionShape> shape;
@@ -137,6 +135,69 @@ bool PhysicsBridge::getRigidBodyTransform(const std::string& name,
return true;
}
namespace detail {
std::array<float, 3> ReadVector3(lua_State* L, int index) {
std::array<float, 3> result{};
int absIndex = lua_absindex(L, index);
size_t len = lua_rawlen(L, absIndex);
if (len != 3) {
throw std::runtime_error("Expected vector with 3 components");
}
for (size_t i = 1; i <= 3; ++i) {
lua_rawgeti(L, absIndex, static_cast<int>(i));
if (!lua_isnumber(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Vector component is not a number");
}
result[i - 1] = static_cast<float>(lua_tonumber(L, -1));
lua_pop(L, 1);
}
return result;
}
std::array<float, 16> ReadMatrix(lua_State* L, int index) {
std::array<float, 16> result{};
int absIndex = lua_absindex(L, index);
size_t len = lua_rawlen(L, absIndex);
if (len != 16) {
throw std::runtime_error("Expected 4x4 matrix with 16 components");
}
for (size_t i = 1; i <= 16; ++i) {
lua_rawgeti(L, absIndex, static_cast<int>(i));
if (!lua_isnumber(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Matrix component is not a number");
}
result[i - 1] = static_cast<float>(lua_tonumber(L, -1));
lua_pop(L, 1);
}
return result;
}
std::array<float, 4> ReadQuaternion(lua_State* L, int index) {
std::array<float, 4> result{};
int absIndex = lua_absindex(L, index);
size_t len = lua_rawlen(L, absIndex);
if (len != 4) {
throw std::runtime_error("Expected quaternion with 4 components");
}
for (size_t i = 1; i <= 4; ++i) {
lua_rawgeti(L, absIndex, static_cast<int>(i));
if (!lua_isnumber(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Quaternion component is not a number");
}
result[i - 1] = static_cast<float>(lua_tonumber(L, -1));
lua_pop(L, 1);
}
return result;
}
} // namespace detail
namespace {
struct MeshPayload {
std::vector<std::array<float, 3>> positions;
std::vector<std::array<float, 3>> colors;
@@ -190,9 +251,9 @@ bool TryLoadMeshPayload(const CubeScript* script,
aiColor3D materialColor = defaultColor;
if (mesh->mMaterialIndex < scene->mNumMaterials) {
const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
aiColor3D diffuse;
aiColor4D diffuse;
if (material && material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse) == AI_SUCCESS) {
materialColor = diffuse;
materialColor = aiColor3D(diffuse.r, diffuse.g, diffuse.b);
}
}
@@ -202,7 +263,8 @@ bool TryLoadMeshPayload(const CubeScript* script,
aiColor3D color = materialColor;
if (mesh->HasVertexColors(0) && mesh->mColors[0]) {
color = mesh->mColors[0][i];
const aiColor4D& vertexColor = mesh->mColors[0][i];
color = aiColor3D(vertexColor.r, vertexColor.g, vertexColor.b);
}
payload.colors.push_back({color.r, color.g, color.b});
}
@@ -298,10 +360,10 @@ int LuaPhysicsCreateBox(lua_State* L) {
if (!lua_istable(L, 2) || !lua_istable(L, 4) || !lua_istable(L, 5)) {
luaL_error(L, "physics_create_box expects vector tables for half extents, origin, and rotation");
}
std::array<float, 3> halfExtents = ReadVector3(L, 2);
std::array<float, 3> halfExtents = detail::ReadVector3(L, 2);
float mass = static_cast<float>(luaL_checknumber(L, 3));
std::array<float, 3> origin = ReadVector3(L, 4);
std::array<float, 4> rotation = ReadQuaternion(L, 5);
std::array<float, 3> origin = detail::ReadVector3(L, 4);
std::array<float, 4> rotation = detail::ReadQuaternion(L, 5);
btTransform transform;
transform.setIdentity();
@@ -370,8 +432,8 @@ int LuaPhysicsGetTransform(lua_State* L) {
}
int LuaGlmMatrixFromTransform(lua_State* L) {
std::array<float, 3> translation = CubeScript::ReadVector3(L, 1);
std::array<float, 4> rotation = CubeScript::ReadQuaternion(L, 2);
std::array<float, 3> translation = detail::ReadVector3(L, 1);
std::array<float, 4> rotation = detail::ReadQuaternion(L, 2);
glm::vec3 pos = ToVec3(translation);
glm::quat quat = ToQuat(rotation);
glm::mat4 matrix = glm::translate(glm::mat4(1.0f), pos) * glm::mat4_cast(quat);
@@ -553,7 +615,7 @@ std::array<float, 16> CubeScript::ComputeModelMatrix(int functionRef, float time
throw std::runtime_error("'compute_model_matrix' did not return a table");
}
std::array<float, 16> matrix = ReadMatrix(L_, -1);
std::array<float, 16> matrix = detail::ReadMatrix(L_, -1);
lua_pop(L_, 1);
return matrix;
}
@@ -574,7 +636,7 @@ std::array<float, 16> CubeScript::GetViewProjectionMatrix(float aspect) {
lua_pop(L_, 1);
throw std::runtime_error("'get_view_projection' did not return a table");
}
std::array<float, 16> matrix = ReadMatrix(L_, -1);
std::array<float, 16> matrix = detail::ReadMatrix(L_, -1);
lua_pop(L_, 1);
return matrix;
}
@@ -600,11 +662,11 @@ std::vector<core::Vertex> CubeScript::ReadVertexArray(lua_State* L, int index) {
core::Vertex vertex{};
lua_getfield(L, vertexIndex, "position");
vertex.position = ReadVector3(L, -1);
vertex.position = detail::ReadVector3(L, -1);
lua_pop(L, 1);
lua_getfield(L, vertexIndex, "color");
vertex.color = ReadVector3(L, -1);
vertex.color = detail::ReadVector3(L, -1);
lua_pop(L, 1);
lua_pop(L, 1);
@@ -697,63 +759,6 @@ CubeScript::ShaderPaths CubeScript::ReadShaderPathsTable(lua_State* L, int index
return paths;
}
std::array<float, 3> CubeScript::ReadVector3(lua_State* L, int index) {
std::array<float, 3> result{};
int absIndex = lua_absindex(L, index);
size_t len = lua_rawlen(L, absIndex);
if (len != 3) {
throw std::runtime_error("Expected vector with 3 components");
}
for (size_t i = 1; i <= 3; ++i) {
lua_rawgeti(L, absIndex, static_cast<int>(i));
if (!lua_isnumber(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Vector component is not a number");
}
result[i - 1] = static_cast<float>(lua_tonumber(L, -1));
lua_pop(L, 1);
}
return result;
}
std::array<float, 16> CubeScript::ReadMatrix(lua_State* L, int index) {
std::array<float, 16> result{};
int absIndex = lua_absindex(L, index);
size_t len = lua_rawlen(L, absIndex);
if (len != 16) {
throw std::runtime_error("Expected 4x4 matrix with 16 components");
}
for (size_t i = 1; i <= 16; ++i) {
lua_rawgeti(L, absIndex, static_cast<int>(i));
if (!lua_isnumber(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Matrix component is not a number");
}
result[i - 1] = static_cast<float>(lua_tonumber(L, -1));
lua_pop(L, 1);
}
return result;
}
std::array<float, 4> CubeScript::ReadQuaternion(lua_State* L, int index) {
std::array<float, 4> result{};
int absIndex = lua_absindex(L, index);
size_t len = lua_rawlen(L, absIndex);
if (len != 4) {
throw std::runtime_error("Expected quaternion with 4 components");
}
for (size_t i = 1; i <= 4; ++i) {
lua_rawgeti(L, absIndex, static_cast<int>(i));
if (!lua_isnumber(L, -1)) {
lua_pop(L, 1);
throw std::runtime_error("Quaternion component is not a number");
}
result[i - 1] = static_cast<float>(lua_tonumber(L, -1));
lua_pop(L, 1);
}
return result;
}
std::string CubeScript::LuaErrorMessage(lua_State* L) {
const char* message = lua_tostring(L, -1);
return message ? message : "unknown lua error";

View File

@@ -97,9 +97,6 @@ public:
PhysicsBridge& GetPhysicsBridge();
private:
static std::array<float, 3> ReadVector3(lua_State* L, int index);
static std::array<float, 16> ReadMatrix(lua_State* L, int index);
static std::array<float, 4> ReadQuaternion(lua_State* L, int index);
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);