mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-25 14:15:02 +00:00
288 lines
8.2 KiB
Lua
288 lines
8.2 KiB
Lua
local Gui = require("gui")
|
|
local math3d = require("math3d")
|
|
|
|
local ctx = Gui.newContext()
|
|
local input = Gui.newInputState()
|
|
local statusMessage = "Select a clip to play"
|
|
|
|
local function findScriptDirectory()
|
|
local info = debug.getinfo(1, "S")
|
|
local source = info.source or ""
|
|
if source:sub(1, 1) == "@" then
|
|
source = source:sub(2)
|
|
end
|
|
local dir = source:match("(.+)[/\\]")
|
|
return dir or "."
|
|
end
|
|
|
|
local scriptDir = findScriptDirectory()
|
|
local isWindows = package.config:sub(1, 1) == "\\"
|
|
|
|
local function joinPath(...)
|
|
local parts = { ... }
|
|
if #parts == 0 then
|
|
return ""
|
|
end
|
|
local sep = package.config:sub(1, 1)
|
|
local path = parts[1] or ""
|
|
for i = 2, #parts do
|
|
local part = parts[i] or ""
|
|
if part ~= "" then
|
|
path = path:gsub("[/\\]+$", "")
|
|
part = part:gsub("^[/\\]+", "")
|
|
if part ~= "" then
|
|
if path == "" then
|
|
path = part
|
|
else
|
|
path = path .. sep .. part
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return path
|
|
end
|
|
|
|
local function escapeForCommand(text)
|
|
return (text or ""):gsub('"', '\\"')
|
|
end
|
|
|
|
local function listDirectory(fullPath)
|
|
local command
|
|
local escaped = escapeForCommand(fullPath)
|
|
if isWindows then
|
|
command = string.format('dir /b /a:-d "%s"', escaped)
|
|
else
|
|
command = string.format('ls -1 "%s"', escaped)
|
|
end
|
|
local handle = io.popen(command)
|
|
if not handle then
|
|
return {}
|
|
end
|
|
local entries = {}
|
|
for line in handle:lines() do
|
|
local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")
|
|
if trimmed ~= "" then
|
|
table.insert(entries, trimmed)
|
|
end
|
|
end
|
|
handle:close()
|
|
return entries
|
|
end
|
|
|
|
local function collectOggFiles(relativeDir)
|
|
local fullDir = joinPath(scriptDir, relativeDir)
|
|
local entries = listDirectory(fullDir)
|
|
local clips = {}
|
|
for _, entry in ipairs(entries) do
|
|
local lower = entry:lower()
|
|
if lower:sub(-4) == ".ogg" then
|
|
local candidate = joinPath(fullDir, entry)
|
|
local handle = io.open(candidate, "rb")
|
|
if handle then
|
|
handle:close()
|
|
table.insert(clips, entry)
|
|
end
|
|
end
|
|
end
|
|
table.sort(clips, function(a, b)
|
|
return a:lower() < b:lower()
|
|
end)
|
|
return clips
|
|
end
|
|
|
|
local function prettyClipName(name)
|
|
local base = name:gsub("%.[^%.]+$", "")
|
|
base = base:gsub("[_%-]+", " ")
|
|
base = base:gsub("(%S+)", function(word)
|
|
local first = word:sub(1, 1)
|
|
local rest = word:sub(2)
|
|
return first:upper() .. rest:lower()
|
|
end)
|
|
return base
|
|
end
|
|
|
|
local function widgetIdForClip(categoryIndex, clipName)
|
|
local sanitized = clipName:gsub("[^%w]+", "_")
|
|
return "clip_" .. categoryIndex .. "_" .. sanitized
|
|
end
|
|
|
|
local categories = {
|
|
{
|
|
name = "Sound Effects",
|
|
relativeDir = "assets/audio/sfx",
|
|
files = collectOggFiles("assets/audio/sfx"),
|
|
},
|
|
{
|
|
name = "Speech",
|
|
relativeDir = "assets/audio/tts",
|
|
files = collectOggFiles("assets/audio/tts"),
|
|
},
|
|
}
|
|
|
|
local function playClip(relativeDir, clipName)
|
|
local label = prettyClipName(clipName)
|
|
if type(audio_play_sound) ~= "function" then
|
|
statusMessage = "Audio playback is unavailable"
|
|
return
|
|
end
|
|
local clipPath = relativeDir .. "/" .. clipName
|
|
local ok, err = pcall(audio_play_sound, clipPath, false)
|
|
if ok then
|
|
statusMessage = "Playing \"" .. label .. "\""
|
|
else
|
|
statusMessage = "Failed to play \"" .. label .. "\": " .. tostring(err)
|
|
end
|
|
end
|
|
|
|
local function drawSoundboardPanel()
|
|
local panel = { x = 16, y = 16, width = 644, height = 520 }
|
|
ctx:pushRect(panel, {
|
|
color = { 0.06, 0.07, 0.09, 0.95 },
|
|
borderColor = { 0.35, 0.38, 0.42, 1.0 },
|
|
})
|
|
Gui.text(ctx, { x = panel.x + 20, y = panel.y + 20, width = panel.width - 40, height = 24 },
|
|
"Audio Soundboard", {
|
|
fontSize = 24,
|
|
color = { 0.96, 0.96, 0.97, 1.0 },
|
|
alignX = "left",
|
|
})
|
|
Gui.text(ctx, { x = panel.x + 20, y = panel.y + 46, width = panel.width - 40, height = 18 },
|
|
"Trigger generated SFX or Piper TTS clips from the assets folder.", {
|
|
fontSize = 14,
|
|
color = { 0.7, 0.75, 0.8, 1.0 },
|
|
alignX = "left",
|
|
})
|
|
|
|
local columnY = panel.y + 80
|
|
local columnWidth = 300
|
|
local columnSpacing = 24
|
|
local buttonHeight = 36
|
|
local buttonSpacing = 12
|
|
|
|
for index, category in ipairs(categories) do
|
|
local columnX = panel.x + 20 + (index - 1) * (columnWidth + columnSpacing)
|
|
Gui.text(ctx, { x = columnX, y = columnY, width = columnWidth, height = 28 }, category.name, {
|
|
fontSize = 20,
|
|
color = { 0.9, 0.9, 0.95, 1.0 },
|
|
alignX = "left",
|
|
})
|
|
|
|
local buttonY = columnY + 32
|
|
if #category.files == 0 then
|
|
Gui.text(ctx, { x = columnX, y = buttonY, width = columnWidth, height = buttonHeight },
|
|
"No clips available", {
|
|
color = { 0.5, 0.55, 0.6, 1.0 },
|
|
alignX = "left",
|
|
alignY = "center",
|
|
})
|
|
else
|
|
for _, clipName in ipairs(category.files) do
|
|
local label = prettyClipName(clipName)
|
|
if Gui.button(ctx, widgetIdForClip(index, clipName), {
|
|
x = columnX,
|
|
y = buttonY,
|
|
width = columnWidth,
|
|
height = buttonHeight,
|
|
}, label) then
|
|
playClip(category.relativeDir, clipName)
|
|
end
|
|
buttonY = buttonY + buttonHeight + buttonSpacing
|
|
end
|
|
end
|
|
end
|
|
|
|
Gui.text(ctx, {
|
|
x = panel.x + 20,
|
|
y = panel.y + panel.height - 34,
|
|
width = panel.width - 40,
|
|
height = 24,
|
|
}, statusMessage, {
|
|
fontSize = 14,
|
|
color = { 0.6, 0.8, 1.0, 1.0 },
|
|
alignX = "left",
|
|
alignY = "center",
|
|
})
|
|
end
|
|
|
|
local cubeVertices = {
|
|
{ position = { -1.0, -1.0, -1.0 }, color = { 1.0, 0.2, 0.4 } },
|
|
{ position = { 1.0, -1.0, -1.0 }, color = { 0.2, 0.9, 0.4 } },
|
|
{ position = { 1.0, 1.0, -1.0 }, color = { 0.3, 0.4, 0.9 } },
|
|
{ position = { -1.0, 1.0, -1.0 }, color = { 1.0, 0.8, 0.2 } },
|
|
{ position = { -1.0, -1.0, 1.0 }, color = { 0.9, 0.4, 0.5 } },
|
|
{ position = { 1.0, -1.0, 1.0 }, color = { 0.4, 0.9, 0.5 } },
|
|
{ position = { 1.0, 1.0, 1.0 }, color = { 0.6, 0.8, 1.0 } },
|
|
{ position = { -1.0, 1.0, 1.0 }, color = { 0.4, 0.4, 0.4 } },
|
|
}
|
|
|
|
local cubeIndices = {
|
|
1, 2, 3, 3, 4, 1,
|
|
5, 6, 7, 7, 8, 5,
|
|
1, 5, 8, 8, 4, 1,
|
|
2, 6, 7, 7, 3, 2,
|
|
4, 3, 7, 7, 8, 4,
|
|
1, 2, 6, 6, 5, 1,
|
|
}
|
|
|
|
local shaderVariants = {
|
|
default = {
|
|
vertex = "shaders/cube.vert.spv",
|
|
fragment = "shaders/cube.frag.spv",
|
|
},
|
|
}
|
|
|
|
local camera = {
|
|
eye = { 2.0, 2.0, 3.0 },
|
|
center = { 0.0, 0.0, 0.0 },
|
|
up = { 0.0, 1.0, 0.0 },
|
|
fov = 0.78,
|
|
near = 0.1,
|
|
far = 10.0,
|
|
}
|
|
|
|
local rotationSpeeds = { x = 0.45, y = 0.65 }
|
|
|
|
local function buildModel(time)
|
|
local yRot = math3d.rotation_y(time * rotationSpeeds.y)
|
|
local xRot = math3d.rotation_x(time * rotationSpeeds.x)
|
|
return math3d.multiply(yRot, xRot)
|
|
end
|
|
|
|
local function createCube(position)
|
|
local function computeModel(time)
|
|
local base = buildModel(time)
|
|
local offset = math3d.translation(position[1], position[2], position[3])
|
|
return math3d.multiply(offset, base)
|
|
end
|
|
return {
|
|
vertices = cubeVertices,
|
|
indices = cubeIndices,
|
|
compute_model_matrix = computeModel,
|
|
shader_key = "default",
|
|
}
|
|
end
|
|
|
|
gui_context = ctx
|
|
gui_input = input
|
|
|
|
function get_scene_objects()
|
|
return { createCube({ 0.0, 0.0, -4.0 }) }
|
|
end
|
|
|
|
function get_shader_paths()
|
|
return shaderVariants
|
|
end
|
|
|
|
function get_view_projection(aspect)
|
|
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)
|
|
end
|
|
|
|
function get_gui_commands()
|
|
ctx:beginFrame(input)
|
|
drawSoundboardPanel()
|
|
ctx:endFrame()
|
|
return ctx:getCommands()
|
|
end
|