implement Milestone 13: audio output with volume limits and concurrent sound management

This commit is contained in:
2026-01-18 16:02:31 +01:00
parent d61b8f0bd8
commit c2e8b8c212
6 changed files with 1236 additions and 1 deletions

View File

@@ -0,0 +1,480 @@
// audio_output.cpp - Audio output implementation for Lua sandbox
// Milestone 13: Safe audio playback with volume limits and concurrent sound management
#include "audio_output.h"
#include <algorithm>
#include <cmath>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
namespace mosis {
// ============================================================================
// SoundPlayer Implementation
// ============================================================================
SoundPlayer::SoundPlayer(int id, const AudioData& audio, const PlaybackConfig& config)
: m_id(id)
, m_audio(audio)
, m_volume(std::clamp(config.volume, 0.0f, 1.0f))
, m_pitch(std::clamp(config.pitch, 0.5f, 2.0f))
, m_pan(std::clamp(config.pan, -1.0f, 1.0f))
, m_loop(config.loop)
, m_position(0.0f)
{
m_state = AudioState::Playing;
}
SoundPlayer::~SoundPlayer() {
Stop();
}
void SoundPlayer::Play() {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_state != AudioState::Playing) {
m_state = AudioState::Playing;
}
}
void SoundPlayer::Pause() {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_state == AudioState::Playing) {
m_state = AudioState::Paused;
}
}
void SoundPlayer::Stop() {
std::lock_guard<std::mutex> lock(m_mutex);
m_state = AudioState::Stopped;
m_position = 0.0f;
}
void SoundPlayer::SetVolume(float volume) {
std::lock_guard<std::mutex> lock(m_mutex);
m_volume = std::clamp(volume, 0.0f, 1.0f);
}
void SoundPlayer::SetPitch(float pitch) {
std::lock_guard<std::mutex> lock(m_mutex);
m_pitch = std::clamp(pitch, 0.5f, 2.0f);
}
void SoundPlayer::SetPan(float pan) {
std::lock_guard<std::mutex> lock(m_mutex);
m_pan = std::clamp(pan, -1.0f, 1.0f);
}
void SoundPlayer::SimulateEnd() {
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_loop) {
m_position = 0.0f;
return;
}
m_state = AudioState::Stopped;
m_position = 1.0f;
}
if (m_on_end) {
m_on_end();
}
}
// ============================================================================
// AudioOutputInterface Implementation
// ============================================================================
AudioOutputInterface::AudioOutputInterface(const std::string& app_id)
: m_app_id(app_id)
{
}
AudioOutputInterface::~AudioOutputInterface() {
Shutdown();
}
std::shared_ptr<SoundPlayer> AudioOutputInterface::Play(const AudioData& audio, const PlaybackConfig& config, std::string& error) {
std::lock_guard<std::mutex> lock(m_mutex);
// Clean up stopped players first
CleanupStoppedPlayers();
// Check concurrent limit
if (m_players.size() >= MAX_CONCURRENT_SOUNDS) {
error = "Maximum concurrent sounds limit reached (10)";
return nullptr;
}
// Validate audio data
if (audio.data.empty()) {
error = "Audio data is empty";
return nullptr;
}
// Create player
int id = m_next_player_id++;
auto player = std::make_shared<SoundPlayer>(id, audio, config);
// Set up cleanup callback
std::weak_ptr<SoundPlayer> weak_player = player;
player->SetOnEnd([this, id]() {
// Player ended, it will be cleaned up on next Play() call
});
m_players[id] = player;
return player;
}
void AudioOutputInterface::StopPlayer(int player_id) {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_players.find(player_id);
if (it != m_players.end()) {
it->second->Stop();
m_players.erase(it);
}
}
void AudioOutputInterface::StopAll() {
std::lock_guard<std::mutex> lock(m_mutex);
for (auto& [id, player] : m_players) {
player->Stop();
}
m_players.clear();
}
size_t AudioOutputInterface::GetActivePlayerCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
size_t count = 0;
for (const auto& [id, player] : m_players) {
if (player->GetState() != AudioState::Stopped) {
count++;
}
}
return count;
}
void AudioOutputInterface::SetSystemVolume(float volume) {
m_system_volume = std::clamp(volume, 0.0f, 1.0f);
}
void AudioOutputInterface::Shutdown() {
StopAll();
}
std::shared_ptr<SoundPlayer> AudioOutputInterface::GetPlayer(int id) {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_players.find(id);
if (it != m_players.end()) {
return it->second;
}
return nullptr;
}
void AudioOutputInterface::CleanupStoppedPlayers() {
// Called with lock held
for (auto it = m_players.begin(); it != m_players.end();) {
if (it->second->GetState() == AudioState::Stopped) {
it = m_players.erase(it);
} else {
++it;
}
}
}
// ============================================================================
// Lua API Implementation
// ============================================================================
// Weak reference to player stored in userdata
struct LuaPlayerRef {
std::weak_ptr<SoundPlayer> player;
AudioOutputInterface* audio;
};
static const char* PLAYER_METATABLE = "mosis.SoundPlayer";
static LuaPlayerRef* GetPlayerRef(lua_State* L, int idx) {
return static_cast<LuaPlayerRef*>(luaL_checkudata(L, idx, PLAYER_METATABLE));
}
static std::shared_ptr<SoundPlayer> GetPlayer(lua_State* L, int idx) {
auto ref = GetPlayerRef(L, idx);
return ref->player.lock();
}
// Player methods
static int LuaPlayer_pause(lua_State* L) {
auto player = GetPlayer(L, 1);
if (player) {
player->Pause();
}
return 0;
}
static int LuaPlayer_resume(lua_State* L) {
auto player = GetPlayer(L, 1);
if (player) {
player->Play();
}
return 0;
}
static int LuaPlayer_stop(lua_State* L) {
auto ref = GetPlayerRef(L, 1);
auto player = ref->player.lock();
if (player && ref->audio) {
ref->audio->StopPlayer(player->GetId());
}
return 0;
}
static int LuaPlayer_setVolume(lua_State* L) {
auto player = GetPlayer(L, 1);
if (player) {
float volume = static_cast<float>(luaL_checknumber(L, 2));
player->SetVolume(volume);
}
return 0;
}
static int LuaPlayer_setPitch(lua_State* L) {
auto player = GetPlayer(L, 1);
if (player) {
float pitch = static_cast<float>(luaL_checknumber(L, 2));
player->SetPitch(pitch);
}
return 0;
}
static int LuaPlayer_setPan(lua_State* L) {
auto player = GetPlayer(L, 1);
if (player) {
float pan = static_cast<float>(luaL_checknumber(L, 2));
player->SetPan(pan);
}
return 0;
}
static int LuaPlayer_getState(lua_State* L) {
auto player = GetPlayer(L, 1);
if (!player) {
lua_pushstring(L, "stopped");
return 1;
}
switch (player->GetState()) {
case AudioState::Playing:
lua_pushstring(L, "playing");
break;
case AudioState::Paused:
lua_pushstring(L, "paused");
break;
case AudioState::Stopped:
default:
lua_pushstring(L, "stopped");
break;
}
return 1;
}
static int LuaPlayer_getPosition(lua_State* L) {
auto player = GetPlayer(L, 1);
if (player) {
lua_pushnumber(L, player->GetPosition());
} else {
lua_pushnumber(L, 0.0);
}
return 1;
}
static int LuaPlayer_gc(lua_State* L) {
auto ref = GetPlayerRef(L, 1);
ref->~LuaPlayerRef();
return 0;
}
static const luaL_Reg player_methods[] = {
{"pause", LuaPlayer_pause},
{"resume", LuaPlayer_resume},
{"stop", LuaPlayer_stop},
{"setVolume", LuaPlayer_setVolume},
{"setPitch", LuaPlayer_setPitch},
{"setPan", LuaPlayer_setPan},
{"getState", LuaPlayer_getState},
{"getPosition", LuaPlayer_getPosition},
{nullptr, nullptr}
};
static void CreatePlayerMetatable(lua_State* L) {
luaL_newmetatable(L, PLAYER_METATABLE);
// __index = methods table
lua_newtable(L);
luaL_setfuncs(L, player_methods, 0);
lua_setfield(L, -2, "__index");
// __gc
lua_pushcfunction(L, LuaPlayer_gc);
lua_setfield(L, -2, "__gc");
lua_pop(L, 1);
}
static void PushPlayer(lua_State* L, std::shared_ptr<SoundPlayer> player, AudioOutputInterface* audio) {
auto ref = static_cast<LuaPlayerRef*>(lua_newuserdata(L, sizeof(LuaPlayerRef)));
new (ref) LuaPlayerRef{player, audio};
luaL_setmetatable(L, PLAYER_METATABLE);
}
// audio.play(soundData, config)
static int LuaAudio_play(lua_State* L) {
// Get audio interface from upvalue
auto audio = static_cast<AudioOutputInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!audio) {
lua_pushnil(L);
lua_pushstring(L, "Audio interface not available");
return 2;
}
// Parse audio data from first argument (table)
AudioData audioData;
if (lua_istable(L, 1)) {
lua_getfield(L, 1, "data");
if (lua_isstring(L, -1)) {
size_t len;
const char* data = lua_tolstring(L, -1, &len);
audioData.data.assign(data, data + len);
}
lua_pop(L, 1);
lua_getfield(L, 1, "sampleRate");
if (lua_isnumber(L, -1)) {
audioData.sample_rate = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
lua_getfield(L, 1, "channels");
if (lua_isnumber(L, -1)) {
audioData.channels = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
lua_getfield(L, 1, "bitsPerSample");
if (lua_isnumber(L, -1)) {
audioData.bits_per_sample = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
}
// Parse playback config from second argument (optional table)
PlaybackConfig config;
if (lua_istable(L, 2)) {
lua_getfield(L, 2, "volume");
if (lua_isnumber(L, -1)) {
config.volume = static_cast<float>(lua_tonumber(L, -1));
}
lua_pop(L, 1);
lua_getfield(L, 2, "loop");
if (lua_isboolean(L, -1)) {
config.loop = lua_toboolean(L, -1);
}
lua_pop(L, 1);
lua_getfield(L, 2, "pitch");
if (lua_isnumber(L, -1)) {
config.pitch = static_cast<float>(lua_tonumber(L, -1));
}
lua_pop(L, 1);
lua_getfield(L, 2, "pan");
if (lua_isnumber(L, -1)) {
config.pan = static_cast<float>(lua_tonumber(L, -1));
}
lua_pop(L, 1);
}
std::string error;
auto player = audio->Play(audioData, config, error);
if (!player) {
lua_pushnil(L);
lua_pushstring(L, error.c_str());
return 2;
}
PushPlayer(L, player, audio);
return 1;
}
// audio.stopAll()
static int LuaAudio_stopAll(lua_State* L) {
auto audio = static_cast<AudioOutputInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (audio) {
audio->StopAll();
}
return 0;
}
// audio.getActiveCount()
static int LuaAudio_getActiveCount(lua_State* L) {
auto audio = static_cast<AudioOutputInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (audio) {
lua_pushinteger(L, static_cast<lua_Integer>(audio->GetActivePlayerCount()));
} else {
lua_pushinteger(L, 0);
}
return 1;
}
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
lua_pushvalue(L, -4);
lua_setfield(L, -2, name);
lua_pop(L, 4);
return;
}
lua_pop(L, 2);
}
lua_pushvalue(L, -2);
lua_setfield(L, -2, name);
lua_pop(L, 2);
}
void RegisterAudioAPI(lua_State* L, AudioOutputInterface* audio) {
// Create player metatable
CreatePlayerMetatable(L);
// Create audio table
lua_newtable(L);
// audio.play
lua_pushlightuserdata(L, audio);
lua_pushcclosure(L, LuaAudio_play, 1);
lua_setfield(L, -2, "play");
// audio.stopAll
lua_pushlightuserdata(L, audio);
lua_pushcclosure(L, LuaAudio_stopAll, 1);
lua_setfield(L, -2, "stopAll");
// audio.getActiveCount
lua_pushlightuserdata(L, audio);
lua_pushcclosure(L, LuaAudio_getActiveCount, 1);
lua_setfield(L, -2, "getActiveCount");
// Set as global (bypasses sandbox proxy)
SetGlobalInRealG(L, "audio");
}
} // namespace mosis

View File

@@ -0,0 +1,130 @@
// audio_output.h - Audio output interface for Lua sandbox
// Milestone 13: Safe audio playback with volume limits and concurrent sound management
#pragma once
#include <string>
#include <memory>
#include <functional>
#include <mutex>
#include <atomic>
#include <vector>
#include <unordered_map>
struct lua_State;
namespace mosis {
enum class AudioState {
Playing,
Paused,
Stopped
};
struct PlaybackConfig {
float volume = 1.0f; // 0.0 to 1.0
bool loop = false;
float pitch = 1.0f; // 0.5 to 2.0
float pan = 0.0f; // -1.0 (left) to 1.0 (right)
};
struct AudioData {
std::vector<uint8_t> data;
int sample_rate = 44100;
int channels = 1;
int bits_per_sample = 16;
};
class SoundPlayer {
public:
using EndCallback = std::function<void()>;
SoundPlayer(int id, const AudioData& audio, const PlaybackConfig& config);
~SoundPlayer();
int GetId() const { return m_id; }
AudioState GetState() const { return m_state; }
float GetVolume() const { return m_volume; }
bool IsLooping() const { return m_loop; }
// Control
void Play();
void Pause();
void Stop();
void SetVolume(float volume);
void SetPitch(float pitch);
void SetPan(float pan);
// Callback when playback ends
void SetOnEnd(EndCallback cb) { m_on_end = std::move(cb); }
// For mock mode - simulate playback completion
void SimulateEnd();
// Get playback position (0.0 to 1.0)
float GetPosition() const { return m_position; }
private:
int m_id;
AudioData m_audio;
std::atomic<AudioState> m_state{AudioState::Stopped};
float m_volume = 1.0f;
float m_pitch = 1.0f;
float m_pan = 0.0f;
bool m_loop = false;
float m_position = 0.0f;
EndCallback m_on_end;
mutable std::mutex m_mutex;
};
class AudioOutputInterface {
public:
AudioOutputInterface(const std::string& app_id);
~AudioOutputInterface();
// Play a sound
// Returns player on success, nullptr on failure (sets error)
std::shared_ptr<SoundPlayer> Play(const AudioData& audio, const PlaybackConfig& config, std::string& error);
// Stop a specific player
void StopPlayer(int player_id);
// Stop all sounds for this app
void StopAll();
// Get active player count
size_t GetActivePlayerCount() const;
// Get system volume (0.0 to 1.0)
float GetSystemVolume() const { return m_system_volume; }
// Set system volume (for testing)
void SetSystemVolume(float volume);
// Cleanup on app stop
void Shutdown();
// For testing
void SetMockMode(bool enabled) { m_mock_mode = enabled; }
bool IsMockMode() const { return m_mock_mode; }
// Get player by ID
std::shared_ptr<SoundPlayer> GetPlayer(int id);
private:
std::string m_app_id;
std::unordered_map<int, std::shared_ptr<SoundPlayer>> m_players;
mutable std::mutex m_mutex;
bool m_mock_mode = true;
float m_system_volume = 1.0f;
int m_next_player_id = 1;
static constexpr int MAX_CONCURRENT_SOUNDS = 10;
void CleanupStoppedPlayers();
};
// Register audio.* APIs as globals
void RegisterAudioAPI(lua_State* L, AudioOutputInterface* audio);
} // namespace mosis