Replace module-level audio functions with proper OOP API: - mcrfpy.Sound: Wraps sf::SoundBuffer + sf::Sound for short effects - mcrfpy.Music: Wraps sf::Music for streaming long tracks - Both support: volume, loop, playing, duration, play/pause/stop - Music adds position property for seeking Add mcrfpy.keyboard singleton for real-time modifier state: - shift, ctrl, alt, system properties (bool, read-only) - Queries sf::Keyboard::isKeyPressed() directly Add mcrfpy.__version__ = "1.0.0" for version identity Remove old audio API entirely (no deprecation - unused in codebase): - createSoundBuffer, loadMusic, playSound - setMusicVolume, getMusicVolume, setSoundVolume, getSoundVolume closes #66, closes #160, closes #164 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
335efc5514
commit
c025cd7da3
11 changed files with 1110 additions and 201 deletions
|
|
@ -9,6 +9,10 @@
|
|||
#include "PyWindow.h"
|
||||
#include "PySceneObject.h"
|
||||
#include "PyFOV.h"
|
||||
#include "PySound.h"
|
||||
#include "PyMusic.h"
|
||||
#include "PyKeyboard.h"
|
||||
#include "McRogueFaceVersion.h"
|
||||
#include "GameEngine.h"
|
||||
#include "ImGuiConsole.h"
|
||||
#include "BenchmarkLogger.h"
|
||||
|
|
@ -23,10 +27,6 @@
|
|||
#include <cstring>
|
||||
#include <libtcod.h>
|
||||
|
||||
std::vector<sf::SoundBuffer>* McRFPy_API::soundbuffers = nullptr;
|
||||
sf::Music* McRFPy_API::music = nullptr;
|
||||
sf::Sound* McRFPy_API::sfx = nullptr;
|
||||
|
||||
std::shared_ptr<PyFont> McRFPy_API::default_font;
|
||||
std::shared_ptr<PyTexture> McRFPy_API::default_texture;
|
||||
PyObject* McRFPy_API::mcrf_module;
|
||||
|
|
@ -102,62 +102,6 @@ static PyTypeObject McRFPyModuleType = {
|
|||
|
||||
static PyMethodDef mcrfpyMethods[] = {
|
||||
|
||||
{"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS,
|
||||
MCRF_FUNCTION(createSoundBuffer,
|
||||
MCRF_SIG("(filename: str)", "int"),
|
||||
MCRF_DESC("Load a sound effect from a file and return its buffer ID."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("filename", "Path to the sound file (WAV, OGG, FLAC)")
|
||||
MCRF_RETURNS("int: Buffer ID for use with playSound()")
|
||||
MCRF_RAISES("RuntimeError", "If the file cannot be loaded")
|
||||
)},
|
||||
{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS,
|
||||
MCRF_FUNCTION(loadMusic,
|
||||
MCRF_SIG("(filename: str)", "None"),
|
||||
MCRF_DESC("Load and immediately play background music from a file."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("filename", "Path to the music file (WAV, OGG, FLAC)")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_NOTE("Only one music track can play at a time. Loading new music stops the current track.")
|
||||
)},
|
||||
{"setMusicVolume", McRFPy_API::_setMusicVolume, METH_VARARGS,
|
||||
MCRF_FUNCTION(setMusicVolume,
|
||||
MCRF_SIG("(volume: int)", "None"),
|
||||
MCRF_DESC("Set the global music volume."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)")
|
||||
MCRF_RETURNS("None")
|
||||
)},
|
||||
{"setSoundVolume", McRFPy_API::_setSoundVolume, METH_VARARGS,
|
||||
MCRF_FUNCTION(setSoundVolume,
|
||||
MCRF_SIG("(volume: int)", "None"),
|
||||
MCRF_DESC("Set the global sound effects volume."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("volume", "Volume level from 0 (silent) to 100 (full volume)")
|
||||
MCRF_RETURNS("None")
|
||||
)},
|
||||
{"playSound", McRFPy_API::_playSound, METH_VARARGS,
|
||||
MCRF_FUNCTION(playSound,
|
||||
MCRF_SIG("(buffer_id: int)", "None"),
|
||||
MCRF_DESC("Play a sound effect using a previously loaded buffer."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("buffer_id", "Sound buffer ID returned by createSoundBuffer()")
|
||||
MCRF_RETURNS("None")
|
||||
MCRF_RAISES("RuntimeError", "If the buffer ID is invalid")
|
||||
)},
|
||||
{"getMusicVolume", McRFPy_API::_getMusicVolume, METH_NOARGS,
|
||||
MCRF_FUNCTION(getMusicVolume,
|
||||
MCRF_SIG("()", "int"),
|
||||
MCRF_DESC("Get the current music volume level."),
|
||||
MCRF_RETURNS("int: Current volume (0-100)")
|
||||
)},
|
||||
{"getSoundVolume", McRFPy_API::_getSoundVolume, METH_NOARGS,
|
||||
MCRF_FUNCTION(getSoundVolume,
|
||||
MCRF_SIG("()", "int"),
|
||||
MCRF_DESC("Get the current sound effects volume level."),
|
||||
MCRF_RETURNS("int: Current volume (0-100)")
|
||||
)},
|
||||
|
||||
{"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS,
|
||||
MCRF_FUNCTION(sceneUI,
|
||||
MCRF_SIG("(scene: str = None)", "list"),
|
||||
|
|
@ -412,6 +356,13 @@ PyObject* PyInit_mcrfpy()
|
|||
/*scene class*/
|
||||
&PySceneType,
|
||||
|
||||
/*audio (#66)*/
|
||||
&PySoundType,
|
||||
&PyMusicType,
|
||||
|
||||
/*keyboard state (#160)*/
|
||||
&PyKeyboardType,
|
||||
|
||||
nullptr};
|
||||
|
||||
// Set up PyWindowType methods and getsetters before PyType_Ready
|
||||
|
|
@ -454,7 +405,16 @@ PyObject* PyInit_mcrfpy()
|
|||
// These will be set later when the window is created
|
||||
PyModule_AddObject(m, "default_font", Py_None);
|
||||
PyModule_AddObject(m, "default_texture", Py_None);
|
||||
|
||||
|
||||
// Add keyboard singleton (#160)
|
||||
PyObject* keyboard_instance = PyObject_CallObject((PyObject*)&PyKeyboardType, NULL);
|
||||
if (keyboard_instance) {
|
||||
PyModule_AddObject(m, "keyboard", keyboard_instance);
|
||||
}
|
||||
|
||||
// Add version string (#164)
|
||||
PyModule_AddStringConstant(m, "__version__", MCRFPY_VERSION);
|
||||
|
||||
// Add FOV enum class (uses Python's IntEnum) (#114)
|
||||
PyObject* fov_class = PyFOV::create_enum_class(m);
|
||||
if (!fov_class) {
|
||||
|
|
@ -821,23 +781,7 @@ void McRFPy_API::executeScript(std::string filename)
|
|||
|
||||
void McRFPy_API::api_shutdown()
|
||||
{
|
||||
// Clean up audio resources in correct order
|
||||
if (sfx) {
|
||||
sfx->stop();
|
||||
delete sfx;
|
||||
sfx = nullptr;
|
||||
}
|
||||
if (music) {
|
||||
music->stop();
|
||||
delete music;
|
||||
music = nullptr;
|
||||
}
|
||||
if (soundbuffers) {
|
||||
soundbuffers->clear();
|
||||
delete soundbuffers;
|
||||
soundbuffers = nullptr;
|
||||
}
|
||||
|
||||
// Audio cleanup now handled by Python garbage collection (Sound/Music classes)
|
||||
Py_Finalize();
|
||||
}
|
||||
|
||||
|
|
@ -869,86 +813,7 @@ PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) {
|
|||
}
|
||||
*/
|
||||
|
||||
PyObject* McRFPy_API::_createSoundBuffer(PyObject* self, PyObject* args) {
|
||||
const char *fn_cstr;
|
||||
if (!PyArg_ParseTuple(args, "s", &fn_cstr)) return NULL;
|
||||
// Initialize soundbuffers if needed
|
||||
if (!McRFPy_API::soundbuffers) {
|
||||
McRFPy_API::soundbuffers = new std::vector<sf::SoundBuffer>();
|
||||
}
|
||||
auto b = sf::SoundBuffer();
|
||||
b.loadFromFile(fn_cstr);
|
||||
McRFPy_API::soundbuffers->push_back(b);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
|
||||
const char *fn_cstr;
|
||||
PyObject* loop_obj = Py_False;
|
||||
if (!PyArg_ParseTuple(args, "s|O", &fn_cstr, &loop_obj)) return NULL;
|
||||
// Initialize music if needed
|
||||
if (!McRFPy_API::music) {
|
||||
McRFPy_API::music = new sf::Music();
|
||||
}
|
||||
McRFPy_API::music->stop();
|
||||
McRFPy_API::music->openFromFile(fn_cstr);
|
||||
McRFPy_API::music->setLoop(PyObject_IsTrue(loop_obj));
|
||||
McRFPy_API::music->play();
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
|
||||
int vol;
|
||||
if (!PyArg_ParseTuple(args, "i", &vol)) return NULL;
|
||||
if (!McRFPy_API::music) {
|
||||
McRFPy_API::music = new sf::Music();
|
||||
}
|
||||
McRFPy_API::music->setVolume(vol);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
|
||||
float vol;
|
||||
if (!PyArg_ParseTuple(args, "f", &vol)) return NULL;
|
||||
if (!McRFPy_API::sfx) {
|
||||
McRFPy_API::sfx = new sf::Sound();
|
||||
}
|
||||
McRFPy_API::sfx->setVolume(vol);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_playSound(PyObject* self, PyObject* args) {
|
||||
float index;
|
||||
if (!PyArg_ParseTuple(args, "f", &index)) return NULL;
|
||||
if (!McRFPy_API::soundbuffers || index >= McRFPy_API::soundbuffers->size()) return NULL;
|
||||
if (!McRFPy_API::sfx) {
|
||||
McRFPy_API::sfx = new sf::Sound();
|
||||
}
|
||||
McRFPy_API::sfx->stop();
|
||||
McRFPy_API::sfx->setBuffer((*McRFPy_API::soundbuffers)[index]);
|
||||
McRFPy_API::sfx->play();
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_getMusicVolume(PyObject* self, PyObject* args) {
|
||||
if (!McRFPy_API::music) {
|
||||
return Py_BuildValue("f", 0.0f);
|
||||
}
|
||||
return Py_BuildValue("f", McRFPy_API::music->getVolume());
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
|
||||
if (!McRFPy_API::sfx) {
|
||||
return Py_BuildValue("f", 0.0f);
|
||||
}
|
||||
return Py_BuildValue("f", McRFPy_API::sfx->getVolume());
|
||||
}
|
||||
|
||||
// Removed deprecated audio functions - use mcrfpy.Sound and mcrfpy.Music classes instead (#66)
|
||||
// Removed deprecated player_input, computerTurn, playerTurn functions
|
||||
// These were part of the old turn-based system that is no longer used
|
||||
|
||||
|
|
|
|||
|
|
@ -37,19 +37,6 @@ public:
|
|||
static void REPL_device(FILE * fp, const char *filename);
|
||||
static void REPL();
|
||||
|
||||
static std::vector<sf::SoundBuffer>* soundbuffers;
|
||||
static sf::Music* music;
|
||||
static sf::Sound* sfx;
|
||||
|
||||
|
||||
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
|
||||
static PyObject* _loadMusic(PyObject*, PyObject*);
|
||||
static PyObject* _setMusicVolume(PyObject*, PyObject*);
|
||||
static PyObject* _setSoundVolume(PyObject*, PyObject*);
|
||||
static PyObject* _playSound(PyObject*, PyObject*);
|
||||
static PyObject* _getMusicVolume(PyObject*, PyObject*);
|
||||
static PyObject* _getSoundVolume(PyObject*, PyObject*);
|
||||
|
||||
static PyObject* _sceneUI(PyObject*, PyObject*);
|
||||
|
||||
// scene control
|
||||
|
|
|
|||
4
src/McRogueFaceVersion.h
Normal file
4
src/McRogueFaceVersion.h
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
|
||||
// McRogueFace version string (#164)
|
||||
#define MCRFPY_VERSION "1.0.0"
|
||||
64
src/PyKeyboard.cpp
Normal file
64
src/PyKeyboard.cpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#include "PyKeyboard.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
|
||||
PyObject* PyKeyboard::repr(PyObject* obj)
|
||||
{
|
||||
// Show current state in repr for debugging
|
||||
bool shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RShift);
|
||||
bool ctrl = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RControl);
|
||||
bool alt = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RAlt);
|
||||
bool system = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RSystem);
|
||||
|
||||
std::ostringstream ss;
|
||||
ss << "<Keyboard shift=" << (shift ? "True" : "False")
|
||||
<< " ctrl=" << (ctrl ? "True" : "False")
|
||||
<< " alt=" << (alt ? "True" : "False")
|
||||
<< " system=" << (system ? "True" : "False") << ">";
|
||||
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
}
|
||||
|
||||
PyObject* PyKeyboard::get_shift(PyObject* self, void* closure)
|
||||
{
|
||||
bool pressed = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RShift);
|
||||
return PyBool_FromLong(pressed);
|
||||
}
|
||||
|
||||
PyObject* PyKeyboard::get_ctrl(PyObject* self, void* closure)
|
||||
{
|
||||
bool pressed = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RControl);
|
||||
return PyBool_FromLong(pressed);
|
||||
}
|
||||
|
||||
PyObject* PyKeyboard::get_alt(PyObject* self, void* closure)
|
||||
{
|
||||
bool pressed = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RAlt);
|
||||
return PyBool_FromLong(pressed);
|
||||
}
|
||||
|
||||
PyObject* PyKeyboard::get_system(PyObject* self, void* closure)
|
||||
{
|
||||
bool pressed = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RSystem);
|
||||
return PyBool_FromLong(pressed);
|
||||
}
|
||||
|
||||
PyGetSetDef PyKeyboard::getsetters[] = {
|
||||
{"shift", (getter)PyKeyboard::get_shift, NULL,
|
||||
MCRF_PROPERTY(shift, "True if either Shift key is currently pressed (read-only)."), NULL},
|
||||
{"ctrl", (getter)PyKeyboard::get_ctrl, NULL,
|
||||
MCRF_PROPERTY(ctrl, "True if either Control key is currently pressed (read-only)."), NULL},
|
||||
{"alt", (getter)PyKeyboard::get_alt, NULL,
|
||||
MCRF_PROPERTY(alt, "True if either Alt key is currently pressed (read-only)."), NULL},
|
||||
{"system", (getter)PyKeyboard::get_system, NULL,
|
||||
MCRF_PROPERTY(system, "True if either System key (Win/Cmd) is currently pressed (read-only)."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
35
src/PyKeyboard.h
Normal file
35
src/PyKeyboard.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
|
||||
// Singleton keyboard state object
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
} PyKeyboardObject;
|
||||
|
||||
class PyKeyboard
|
||||
{
|
||||
public:
|
||||
// Python getters - query real-time keyboard state via SFML
|
||||
static PyObject* get_shift(PyObject* self, void* closure);
|
||||
static PyObject* get_ctrl(PyObject* self, void* closure);
|
||||
static PyObject* get_alt(PyObject* self, void* closure);
|
||||
static PyObject* get_system(PyObject* self, void* closure);
|
||||
|
||||
static PyObject* repr(PyObject* obj);
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyKeyboardType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Keyboard",
|
||||
.tp_basicsize = sizeof(PyKeyboardObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_repr = PyKeyboard::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Keyboard state singleton for checking modifier keys"),
|
||||
.tp_getset = PyKeyboard::getsetters,
|
||||
.tp_new = PyType_GenericNew,
|
||||
};
|
||||
}
|
||||
294
src/PyMusic.cpp
Normal file
294
src/PyMusic.cpp
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
#include "PyMusic.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
PyMusic::PyMusic(const std::string& filename)
|
||||
: source(filename), loaded(false)
|
||||
{
|
||||
if (music.openFromFile(filename)) {
|
||||
loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PyMusic::play()
|
||||
{
|
||||
if (loaded) {
|
||||
music.play();
|
||||
}
|
||||
}
|
||||
|
||||
void PyMusic::pause()
|
||||
{
|
||||
if (loaded) {
|
||||
music.pause();
|
||||
}
|
||||
}
|
||||
|
||||
void PyMusic::stop()
|
||||
{
|
||||
if (loaded) {
|
||||
music.stop();
|
||||
}
|
||||
}
|
||||
|
||||
float PyMusic::getVolume() const
|
||||
{
|
||||
return music.getVolume();
|
||||
}
|
||||
|
||||
void PyMusic::setVolume(float vol)
|
||||
{
|
||||
music.setVolume(std::max(0.0f, std::min(100.0f, vol)));
|
||||
}
|
||||
|
||||
bool PyMusic::getLoop() const
|
||||
{
|
||||
return music.getLoop();
|
||||
}
|
||||
|
||||
void PyMusic::setLoop(bool loop)
|
||||
{
|
||||
music.setLoop(loop);
|
||||
}
|
||||
|
||||
bool PyMusic::isPlaying() const
|
||||
{
|
||||
return music.getStatus() == sf::Music::Playing;
|
||||
}
|
||||
|
||||
float PyMusic::getDuration() const
|
||||
{
|
||||
if (!loaded) return 0.0f;
|
||||
return music.getDuration().asSeconds();
|
||||
}
|
||||
|
||||
float PyMusic::getPosition() const
|
||||
{
|
||||
return music.getPlayingOffset().asSeconds();
|
||||
}
|
||||
|
||||
void PyMusic::setPosition(float pos)
|
||||
{
|
||||
music.setPlayingOffset(sf::seconds(pos));
|
||||
}
|
||||
|
||||
PyObject* PyMusic::pyObject()
|
||||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Music");
|
||||
PyObject* obj = PyMusic::pynew(type, Py_None, Py_None);
|
||||
Py_DECREF(type);
|
||||
try {
|
||||
((PyMusicObject*)obj)->data = shared_from_this();
|
||||
}
|
||||
catch (std::bad_weak_ptr& e) {
|
||||
std::cerr << "PyMusic::pyObject() - shared_from_this() failed" << std::endl;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
PyObject* PyMusic::repr(PyObject* obj)
|
||||
{
|
||||
PyMusicObject* self = (PyMusicObject*)obj;
|
||||
std::ostringstream ss;
|
||||
if (!self->data) {
|
||||
ss << "<Music [invalid]>";
|
||||
} else if (!self->data->loaded) {
|
||||
ss << "<Music [failed to load: " << self->data->source << "]>";
|
||||
} else {
|
||||
ss << "<Music source='" << self->data->source << "' duration="
|
||||
<< std::fixed << std::setprecision(2) << self->data->getDuration() << "s>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
}
|
||||
|
||||
Py_hash_t PyMusic::hash(PyObject* obj)
|
||||
{
|
||||
auto self = (PyMusicObject*)obj;
|
||||
return reinterpret_cast<Py_hash_t>(self->data.get());
|
||||
}
|
||||
|
||||
int PyMusic::init(PyMusicObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"filename", nullptr};
|
||||
const char* filename = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &filename)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data = std::make_shared<PyMusic>(filename);
|
||||
|
||||
if (!self->data->loaded) {
|
||||
PyErr_Format(PyExc_RuntimeError, "Failed to load music file: %s", filename);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyMusic::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
return (PyObject*)type->tp_alloc(type, 0);
|
||||
}
|
||||
|
||||
// Python methods
|
||||
PyObject* PyMusic::py_play(PyMusicObject* self, PyObject* args)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
self->data->play();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyMusic::py_pause(PyMusicObject* self, PyObject* args)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
self->data->pause();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyMusic::py_stop(PyMusicObject* self, PyObject* args)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
self->data->stop();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Property getters/setters
|
||||
PyObject* PyMusic::get_volume(PyMusicObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyFloat_FromDouble(self->data->getVolume());
|
||||
}
|
||||
|
||||
int PyMusic::set_volume(PyMusicObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return -1;
|
||||
}
|
||||
float vol = PyFloat_AsDouble(value);
|
||||
if (PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
self->data->setVolume(vol);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyMusic::get_loop(PyMusicObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyBool_FromLong(self->data->getLoop());
|
||||
}
|
||||
|
||||
int PyMusic::set_loop(PyMusicObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return -1;
|
||||
}
|
||||
self->data->setLoop(PyObject_IsTrue(value));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyMusic::get_playing(PyMusicObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyBool_FromLong(self->data->isPlaying());
|
||||
}
|
||||
|
||||
PyObject* PyMusic::get_duration(PyMusicObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyFloat_FromDouble(self->data->getDuration());
|
||||
}
|
||||
|
||||
PyObject* PyMusic::get_position(PyMusicObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyFloat_FromDouble(self->data->getPosition());
|
||||
}
|
||||
|
||||
int PyMusic::set_position(PyMusicObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return -1;
|
||||
}
|
||||
float pos = PyFloat_AsDouble(value);
|
||||
if (PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
self->data->setPosition(pos);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyMusic::get_source(PyMusicObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Music object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyUnicode_FromString(self->data->source.c_str());
|
||||
}
|
||||
|
||||
PyMethodDef PyMusic::methods[] = {
|
||||
{"play", (PyCFunction)PyMusic::py_play, METH_NOARGS,
|
||||
MCRF_METHOD(Music, play,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Start or resume playing the music.")
|
||||
)},
|
||||
{"pause", (PyCFunction)PyMusic::py_pause, METH_NOARGS,
|
||||
MCRF_METHOD(Music, pause,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Pause the music. Use play() to resume from the paused position.")
|
||||
)},
|
||||
{"stop", (PyCFunction)PyMusic::py_stop, METH_NOARGS,
|
||||
MCRF_METHOD(Music, stop,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Stop playing and reset to the beginning.")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyGetSetDef PyMusic::getsetters[] = {
|
||||
{"volume", (getter)PyMusic::get_volume, (setter)PyMusic::set_volume,
|
||||
MCRF_PROPERTY(volume, "Volume level from 0 (silent) to 100 (full volume)."), NULL},
|
||||
{"loop", (getter)PyMusic::get_loop, (setter)PyMusic::set_loop,
|
||||
MCRF_PROPERTY(loop, "Whether the music loops when it reaches the end."), NULL},
|
||||
{"playing", (getter)PyMusic::get_playing, NULL,
|
||||
MCRF_PROPERTY(playing, "True if the music is currently playing (read-only)."), NULL},
|
||||
{"duration", (getter)PyMusic::get_duration, NULL,
|
||||
MCRF_PROPERTY(duration, "Total duration of the music in seconds (read-only)."), NULL},
|
||||
{"position", (getter)PyMusic::get_position, (setter)PyMusic::set_position,
|
||||
MCRF_PROPERTY(position, "Current playback position in seconds. Can be set to seek."), NULL},
|
||||
{"source", (getter)PyMusic::get_source, NULL,
|
||||
MCRF_PROPERTY(source, "Filename path used to load this music (read-only)."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
79
src/PyMusic.h
Normal file
79
src/PyMusic.h
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
|
||||
class PyMusic;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<PyMusic> data;
|
||||
} PyMusicObject;
|
||||
|
||||
class PyMusic : public std::enable_shared_from_this<PyMusic>
|
||||
{
|
||||
private:
|
||||
sf::Music music;
|
||||
std::string source;
|
||||
bool loaded;
|
||||
|
||||
public:
|
||||
PyMusic(const std::string& filename);
|
||||
|
||||
// Playback control
|
||||
void play();
|
||||
void pause();
|
||||
void stop();
|
||||
|
||||
// Properties
|
||||
float getVolume() const;
|
||||
void setVolume(float vol);
|
||||
bool getLoop() const;
|
||||
void setLoop(bool loop);
|
||||
bool isPlaying() const;
|
||||
float getDuration() const;
|
||||
float getPosition() const;
|
||||
void setPosition(float pos);
|
||||
|
||||
// Python interface
|
||||
PyObject* pyObject();
|
||||
static PyObject* repr(PyObject* obj);
|
||||
static Py_hash_t hash(PyObject* obj);
|
||||
static int init(PyMusicObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Python methods
|
||||
static PyObject* py_play(PyMusicObject* self, PyObject* args);
|
||||
static PyObject* py_pause(PyMusicObject* self, PyObject* args);
|
||||
static PyObject* py_stop(PyMusicObject* self, PyObject* args);
|
||||
|
||||
// Python getters/setters
|
||||
static PyObject* get_volume(PyMusicObject* self, void* closure);
|
||||
static int set_volume(PyMusicObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_loop(PyMusicObject* self, void* closure);
|
||||
static int set_loop(PyMusicObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_playing(PyMusicObject* self, void* closure);
|
||||
static PyObject* get_duration(PyMusicObject* self, void* closure);
|
||||
static PyObject* get_position(PyMusicObject* self, void* closure);
|
||||
static int set_position(PyMusicObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_source(PyMusicObject* self, void* closure);
|
||||
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyMusicType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Music",
|
||||
.tp_basicsize = sizeof(PyMusicObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_repr = PyMusic::repr,
|
||||
.tp_hash = PyMusic::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Streaming music object for longer audio tracks"),
|
||||
.tp_methods = PyMusic::methods,
|
||||
.tp_getset = PyMusic::getsetters,
|
||||
.tp_init = (initproc)PyMusic::init,
|
||||
.tp_new = PyType_GenericNew,
|
||||
};
|
||||
}
|
||||
259
src/PySound.cpp
Normal file
259
src/PySound.cpp
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
#include "PySound.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <sstream>
|
||||
|
||||
PySound::PySound(const std::string& filename)
|
||||
: source(filename), loaded(false)
|
||||
{
|
||||
if (buffer.loadFromFile(filename)) {
|
||||
sound.setBuffer(buffer);
|
||||
loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PySound::play()
|
||||
{
|
||||
if (loaded) {
|
||||
sound.play();
|
||||
}
|
||||
}
|
||||
|
||||
void PySound::pause()
|
||||
{
|
||||
if (loaded) {
|
||||
sound.pause();
|
||||
}
|
||||
}
|
||||
|
||||
void PySound::stop()
|
||||
{
|
||||
if (loaded) {
|
||||
sound.stop();
|
||||
}
|
||||
}
|
||||
|
||||
float PySound::getVolume() const
|
||||
{
|
||||
return sound.getVolume();
|
||||
}
|
||||
|
||||
void PySound::setVolume(float vol)
|
||||
{
|
||||
sound.setVolume(std::max(0.0f, std::min(100.0f, vol)));
|
||||
}
|
||||
|
||||
bool PySound::getLoop() const
|
||||
{
|
||||
return sound.getLoop();
|
||||
}
|
||||
|
||||
void PySound::setLoop(bool loop)
|
||||
{
|
||||
sound.setLoop(loop);
|
||||
}
|
||||
|
||||
bool PySound::isPlaying() const
|
||||
{
|
||||
return sound.getStatus() == sf::Sound::Playing;
|
||||
}
|
||||
|
||||
float PySound::getDuration() const
|
||||
{
|
||||
if (!loaded) return 0.0f;
|
||||
return buffer.getDuration().asSeconds();
|
||||
}
|
||||
|
||||
PyObject* PySound::pyObject()
|
||||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sound");
|
||||
PyObject* obj = PySound::pynew(type, Py_None, Py_None);
|
||||
Py_DECREF(type);
|
||||
try {
|
||||
((PySoundObject*)obj)->data = shared_from_this();
|
||||
}
|
||||
catch (std::bad_weak_ptr& e) {
|
||||
std::cerr << "PySound::pyObject() - shared_from_this() failed" << std::endl;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
PyObject* PySound::repr(PyObject* obj)
|
||||
{
|
||||
PySoundObject* self = (PySoundObject*)obj;
|
||||
std::ostringstream ss;
|
||||
if (!self->data) {
|
||||
ss << "<Sound [invalid]>";
|
||||
} else if (!self->data->loaded) {
|
||||
ss << "<Sound [failed to load: " << self->data->source << "]>";
|
||||
} else {
|
||||
ss << "<Sound source='" << self->data->source << "' duration="
|
||||
<< std::fixed << std::setprecision(2) << self->data->getDuration() << "s>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
}
|
||||
|
||||
Py_hash_t PySound::hash(PyObject* obj)
|
||||
{
|
||||
auto self = (PySoundObject*)obj;
|
||||
return reinterpret_cast<Py_hash_t>(self->data.get());
|
||||
}
|
||||
|
||||
int PySound::init(PySoundObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
static const char* keywords[] = {"filename", nullptr};
|
||||
const char* filename = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &filename)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data = std::make_shared<PySound>(filename);
|
||||
|
||||
if (!self->data->loaded) {
|
||||
PyErr_Format(PyExc_RuntimeError, "Failed to load sound file: %s", filename);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PySound::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
return (PyObject*)type->tp_alloc(type, 0);
|
||||
}
|
||||
|
||||
// Python methods
|
||||
PyObject* PySound::py_play(PySoundObject* self, PyObject* args)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
self->data->play();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PySound::py_pause(PySoundObject* self, PyObject* args)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
self->data->pause();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PySound::py_stop(PySoundObject* self, PyObject* args)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
self->data->stop();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Property getters/setters
|
||||
PyObject* PySound::get_volume(PySoundObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyFloat_FromDouble(self->data->getVolume());
|
||||
}
|
||||
|
||||
int PySound::set_volume(PySoundObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return -1;
|
||||
}
|
||||
float vol = PyFloat_AsDouble(value);
|
||||
if (PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
self->data->setVolume(vol);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PySound::get_loop(PySoundObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyBool_FromLong(self->data->getLoop());
|
||||
}
|
||||
|
||||
int PySound::set_loop(PySoundObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return -1;
|
||||
}
|
||||
self->data->setLoop(PyObject_IsTrue(value));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PySound::get_playing(PySoundObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyBool_FromLong(self->data->isPlaying());
|
||||
}
|
||||
|
||||
PyObject* PySound::get_duration(PySoundObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyFloat_FromDouble(self->data->getDuration());
|
||||
}
|
||||
|
||||
PyObject* PySound::get_source(PySoundObject* self, void* closure)
|
||||
{
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Sound object is invalid");
|
||||
return NULL;
|
||||
}
|
||||
return PyUnicode_FromString(self->data->source.c_str());
|
||||
}
|
||||
|
||||
PyMethodDef PySound::methods[] = {
|
||||
{"play", (PyCFunction)PySound::py_play, METH_NOARGS,
|
||||
MCRF_METHOD(Sound, play,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Start or resume playing the sound.")
|
||||
)},
|
||||
{"pause", (PyCFunction)PySound::py_pause, METH_NOARGS,
|
||||
MCRF_METHOD(Sound, pause,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Pause the sound. Use play() to resume from the paused position.")
|
||||
)},
|
||||
{"stop", (PyCFunction)PySound::py_stop, METH_NOARGS,
|
||||
MCRF_METHOD(Sound, stop,
|
||||
MCRF_SIG("()", "None"),
|
||||
MCRF_DESC("Stop playing and reset to the beginning.")
|
||||
)},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyGetSetDef PySound::getsetters[] = {
|
||||
{"volume", (getter)PySound::get_volume, (setter)PySound::set_volume,
|
||||
MCRF_PROPERTY(volume, "Volume level from 0 (silent) to 100 (full volume)."), NULL},
|
||||
{"loop", (getter)PySound::get_loop, (setter)PySound::set_loop,
|
||||
MCRF_PROPERTY(loop, "Whether the sound loops when it reaches the end."), NULL},
|
||||
{"playing", (getter)PySound::get_playing, NULL,
|
||||
MCRF_PROPERTY(playing, "True if the sound is currently playing (read-only)."), NULL},
|
||||
{"duration", (getter)PySound::get_duration, NULL,
|
||||
MCRF_PROPERTY(duration, "Total duration of the sound in seconds (read-only)."), NULL},
|
||||
{"source", (getter)PySound::get_source, NULL,
|
||||
MCRF_PROPERTY(source, "Filename path used to load this sound (read-only)."), NULL},
|
||||
{NULL}
|
||||
};
|
||||
76
src/PySound.h
Normal file
76
src/PySound.h
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
|
||||
class PySound;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<PySound> data;
|
||||
} PySoundObject;
|
||||
|
||||
class PySound : public std::enable_shared_from_this<PySound>
|
||||
{
|
||||
private:
|
||||
sf::SoundBuffer buffer;
|
||||
sf::Sound sound;
|
||||
std::string source;
|
||||
bool loaded;
|
||||
|
||||
public:
|
||||
PySound(const std::string& filename);
|
||||
|
||||
// Playback control
|
||||
void play();
|
||||
void pause();
|
||||
void stop();
|
||||
|
||||
// Properties
|
||||
float getVolume() const;
|
||||
void setVolume(float vol);
|
||||
bool getLoop() const;
|
||||
void setLoop(bool loop);
|
||||
bool isPlaying() const;
|
||||
float getDuration() const;
|
||||
|
||||
// Python interface
|
||||
PyObject* pyObject();
|
||||
static PyObject* repr(PyObject* obj);
|
||||
static Py_hash_t hash(PyObject* obj);
|
||||
static int init(PySoundObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Python methods
|
||||
static PyObject* py_play(PySoundObject* self, PyObject* args);
|
||||
static PyObject* py_pause(PySoundObject* self, PyObject* args);
|
||||
static PyObject* py_stop(PySoundObject* self, PyObject* args);
|
||||
|
||||
// Python getters/setters
|
||||
static PyObject* get_volume(PySoundObject* self, void* closure);
|
||||
static int set_volume(PySoundObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_loop(PySoundObject* self, void* closure);
|
||||
static int set_loop(PySoundObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_playing(PySoundObject* self, void* closure);
|
||||
static PyObject* get_duration(PySoundObject* self, void* closure);
|
||||
static PyObject* get_source(PySoundObject* self, void* closure);
|
||||
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PySoundType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Sound",
|
||||
.tp_basicsize = sizeof(PySoundObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_repr = PySound::repr,
|
||||
.tp_hash = PySound::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Sound effect object for short audio clips"),
|
||||
.tp_methods = PySound::methods,
|
||||
.tp_getset = PySound::getsetters,
|
||||
.tp_init = (initproc)PySound::init,
|
||||
.tp_new = PyType_GenericNew,
|
||||
};
|
||||
}
|
||||
153
stubs/mcrfpy.pyi
153
stubs/mcrfpy.pyi
|
|
@ -67,12 +67,125 @@ class Texture:
|
|||
|
||||
class Font:
|
||||
"""SFML Font Object for text rendering."""
|
||||
|
||||
|
||||
def __init__(self, filename: str) -> None: ...
|
||||
|
||||
|
||||
filename: str
|
||||
family: str
|
||||
|
||||
class Sound:
|
||||
"""Sound effect object for short audio clips.
|
||||
|
||||
Sounds are loaded entirely into memory, making them suitable for
|
||||
short sound effects that need to be played with minimal latency.
|
||||
Multiple Sound instances can play simultaneously.
|
||||
"""
|
||||
|
||||
def __init__(self, filename: str) -> None:
|
||||
"""Load a sound effect from a file.
|
||||
|
||||
Args:
|
||||
filename: Path to the sound file (WAV, OGG, FLAC supported)
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the file cannot be loaded
|
||||
"""
|
||||
...
|
||||
|
||||
volume: float
|
||||
"""Volume level from 0 (silent) to 100 (full volume)."""
|
||||
|
||||
loop: bool
|
||||
"""Whether the sound loops when it reaches the end."""
|
||||
|
||||
playing: bool
|
||||
"""True if the sound is currently playing (read-only)."""
|
||||
|
||||
duration: float
|
||||
"""Total duration of the sound in seconds (read-only)."""
|
||||
|
||||
source: str
|
||||
"""Filename path used to load this sound (read-only)."""
|
||||
|
||||
def play(self) -> None:
|
||||
"""Start or resume playing the sound."""
|
||||
...
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the sound. Use play() to resume."""
|
||||
...
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop playing and reset to the beginning."""
|
||||
...
|
||||
|
||||
class Music:
|
||||
"""Streaming music object for longer audio tracks.
|
||||
|
||||
Music is streamed from disk rather than loaded entirely into memory,
|
||||
making it suitable for longer audio tracks like background music.
|
||||
"""
|
||||
|
||||
def __init__(self, filename: str) -> None:
|
||||
"""Load a music track from a file.
|
||||
|
||||
Args:
|
||||
filename: Path to the music file (WAV, OGG, FLAC supported)
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the file cannot be loaded
|
||||
"""
|
||||
...
|
||||
|
||||
volume: float
|
||||
"""Volume level from 0 (silent) to 100 (full volume)."""
|
||||
|
||||
loop: bool
|
||||
"""Whether the music loops when it reaches the end."""
|
||||
|
||||
playing: bool
|
||||
"""True if the music is currently playing (read-only)."""
|
||||
|
||||
duration: float
|
||||
"""Total duration of the music in seconds (read-only)."""
|
||||
|
||||
position: float
|
||||
"""Current playback position in seconds. Can be set to seek."""
|
||||
|
||||
source: str
|
||||
"""Filename path used to load this music (read-only)."""
|
||||
|
||||
def play(self) -> None:
|
||||
"""Start or resume playing the music."""
|
||||
...
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the music. Use play() to resume."""
|
||||
...
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop playing and reset to the beginning."""
|
||||
...
|
||||
|
||||
class Keyboard:
|
||||
"""Keyboard state singleton for checking modifier keys.
|
||||
|
||||
Access via mcrfpy.keyboard (singleton instance).
|
||||
Queries real-time keyboard state from SFML.
|
||||
"""
|
||||
|
||||
shift: bool
|
||||
"""True if either Shift key is currently pressed (read-only)."""
|
||||
|
||||
ctrl: bool
|
||||
"""True if either Control key is currently pressed (read-only)."""
|
||||
|
||||
alt: bool
|
||||
"""True if either Alt key is currently pressed (read-only)."""
|
||||
|
||||
system: bool
|
||||
"""True if either System key (Win/Cmd) is currently pressed (read-only)."""
|
||||
|
||||
class Drawable:
|
||||
"""Base class for all drawable UI elements."""
|
||||
|
||||
|
|
@ -446,36 +559,16 @@ class Animation:
|
|||
"""Get the current interpolated value."""
|
||||
...
|
||||
|
||||
# Module-level attributes
|
||||
|
||||
__version__: str
|
||||
"""McRogueFace version string (e.g., '1.0.0')."""
|
||||
|
||||
keyboard: Keyboard
|
||||
"""Keyboard state singleton for checking modifier keys."""
|
||||
|
||||
# Module functions
|
||||
|
||||
def createSoundBuffer(filename: str) -> int:
|
||||
"""Load a sound effect from a file and return its buffer ID."""
|
||||
...
|
||||
|
||||
def loadMusic(filename: str) -> None:
|
||||
"""Load and immediately play background music from a file."""
|
||||
...
|
||||
|
||||
def setMusicVolume(volume: int) -> None:
|
||||
"""Set the global music volume (0-100)."""
|
||||
...
|
||||
|
||||
def setSoundVolume(volume: int) -> None:
|
||||
"""Set the global sound effects volume (0-100)."""
|
||||
...
|
||||
|
||||
def playSound(buffer_id: int) -> None:
|
||||
"""Play a sound effect using a previously loaded buffer."""
|
||||
...
|
||||
|
||||
def getMusicVolume() -> int:
|
||||
"""Get the current music volume level (0-100)."""
|
||||
...
|
||||
|
||||
def getSoundVolume() -> int:
|
||||
"""Get the current sound effects volume level (0-100)."""
|
||||
...
|
||||
|
||||
def sceneUI(scene: Optional[str] = None) -> UICollection:
|
||||
"""Get all UI elements for a scene."""
|
||||
...
|
||||
|
|
|
|||
153
tests/unit/audio_and_keyboard_test.py
Normal file
153
tests/unit/audio_and_keyboard_test.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test for Sound, Music, Keyboard classes and __version__ (#66, #160, #164)."""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_version():
|
||||
"""Test that __version__ exists and is a valid semver string."""
|
||||
assert hasattr(mcrfpy, '__version__'), "mcrfpy.__version__ not found"
|
||||
version = mcrfpy.__version__
|
||||
assert isinstance(version, str), f"__version__ should be str, got {type(version)}"
|
||||
parts = version.split('.')
|
||||
assert len(parts) == 3, f"Version should be MAJOR.MINOR.PATCH, got {version}"
|
||||
print(f" Version: {version}")
|
||||
|
||||
def test_keyboard():
|
||||
"""Test Keyboard singleton exists and has expected properties."""
|
||||
assert hasattr(mcrfpy, 'keyboard'), "mcrfpy.keyboard not found"
|
||||
kb = mcrfpy.keyboard
|
||||
|
||||
# Check all modifier properties exist and are bool
|
||||
for prop in ['shift', 'ctrl', 'alt', 'system']:
|
||||
assert hasattr(kb, prop), f"keyboard.{prop} not found"
|
||||
val = getattr(kb, prop)
|
||||
assert isinstance(val, bool), f"keyboard.{prop} should be bool, got {type(val)}"
|
||||
|
||||
print(f" Keyboard state: {kb}")
|
||||
|
||||
def test_sound_class():
|
||||
"""Test Sound class creation and properties."""
|
||||
# Test with a known good file
|
||||
sound = mcrfpy.Sound("assets/sfx/splat1.ogg")
|
||||
|
||||
# Check repr works
|
||||
repr_str = repr(sound)
|
||||
assert 'Sound' in repr_str
|
||||
assert 'splat1.ogg' in repr_str
|
||||
print(f" Sound: {repr_str}")
|
||||
|
||||
# Check default values
|
||||
assert sound.volume == 100.0, f"Default volume should be 100, got {sound.volume}"
|
||||
assert sound.loop == False, f"Default loop should be False, got {sound.loop}"
|
||||
assert sound.playing == False, f"Should not be playing initially"
|
||||
assert sound.duration > 0, f"Duration should be positive, got {sound.duration}"
|
||||
assert sound.source == "assets/sfx/splat1.ogg"
|
||||
|
||||
# Test setting properties (use tolerance for floating point)
|
||||
sound.volume = 50.0
|
||||
assert abs(sound.volume - 50.0) < 0.01, f"Volume should be ~50, got {sound.volume}"
|
||||
|
||||
sound.loop = True
|
||||
assert sound.loop == True
|
||||
|
||||
# Test methods exist (don't actually play in headless)
|
||||
assert callable(sound.play)
|
||||
assert callable(sound.pause)
|
||||
assert callable(sound.stop)
|
||||
|
||||
print(f" Duration: {sound.duration:.3f}s")
|
||||
|
||||
def test_sound_error_handling():
|
||||
"""Test Sound raises on invalid file."""
|
||||
try:
|
||||
sound = mcrfpy.Sound("nonexistent_file.ogg")
|
||||
print(" ERROR: Should have raised RuntimeError")
|
||||
return False
|
||||
except RuntimeError as e:
|
||||
print(f" Correctly raised: {e}")
|
||||
return True
|
||||
|
||||
def test_music_class():
|
||||
"""Test Music class creation and properties."""
|
||||
music = mcrfpy.Music("assets/sfx/splat1.ogg")
|
||||
|
||||
# Check repr works
|
||||
repr_str = repr(music)
|
||||
assert 'Music' in repr_str
|
||||
print(f" Music: {repr_str}")
|
||||
|
||||
# Check default values
|
||||
assert music.volume == 100.0, f"Default volume should be 100, got {music.volume}"
|
||||
assert music.loop == False, f"Default loop should be False, got {music.loop}"
|
||||
assert music.playing == False, f"Should not be playing initially, got {music.playing}"
|
||||
assert music.duration > 0, f"Duration should be positive, got {music.duration}"
|
||||
# Position comparison needs tolerance for floating point
|
||||
assert abs(music.position) < 0.001, f"Position should be ~0, got {music.position}"
|
||||
assert music.source == "assets/sfx/splat1.ogg", f"Source mismatch: {music.source}"
|
||||
|
||||
# Test setting properties (use tolerance for floating point)
|
||||
music.volume = 30.0
|
||||
assert abs(music.volume - 30.0) < 0.01, f"Volume should be ~30, got {music.volume}"
|
||||
|
||||
music.loop = True
|
||||
assert music.loop == True, f"Loop should be True, got {music.loop}"
|
||||
|
||||
# Test position can be set (seek)
|
||||
# Can't really test this without playing, but check it's writable
|
||||
music.position = 0.1
|
||||
|
||||
print(f" Duration: {music.duration:.3f}s")
|
||||
|
||||
def test_music_error_handling():
|
||||
"""Test Music raises on invalid file."""
|
||||
try:
|
||||
music = mcrfpy.Music("nonexistent_file.ogg")
|
||||
print(" ERROR: Should have raised RuntimeError")
|
||||
return False
|
||||
except RuntimeError as e:
|
||||
print(f" Correctly raised: {e}")
|
||||
return True
|
||||
|
||||
def main():
|
||||
print("Testing mcrfpy audio and keyboard features (#66, #160, #164)")
|
||||
print()
|
||||
|
||||
tests = [
|
||||
("__version__", test_version),
|
||||
("keyboard singleton", test_keyboard),
|
||||
("Sound class", test_sound_class),
|
||||
("Sound error handling", test_sound_error_handling),
|
||||
("Music class", test_music_class),
|
||||
("Music error handling", test_music_error_handling),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for name, test_fn in tests:
|
||||
print(f"Testing {name}...")
|
||||
try:
|
||||
result = test_fn()
|
||||
if result is False:
|
||||
failed += 1
|
||||
print(f" FAILED")
|
||||
else:
|
||||
passed += 1
|
||||
print(f" PASSED")
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
print(f" FAILED: {e}")
|
||||
|
||||
print()
|
||||
print(f"Results: {passed} passed, {failed} failed")
|
||||
|
||||
if failed == 0:
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("FAIL")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue