Files
test/extern/audaspace/bindings/python/PyAnimateableProperty.cpp
Jörg Müller f085f88835 Audaspace: porting changes from upstream.
This change introduces animated time stretching and pitch scaling.
It also extends the Python API with AnimateableProperty.
Note: to be used, this still needs rubberband.

Credit: Kacey La
2025-08-23 17:25:38 +02:00

399 lines
15 KiB
C++

/*******************************************************************************
* Copyright 2009-2025 Jörg Müller
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
#include "PyAnimateableProperty.h"
#include "Exception.h"
#include "sequence/AnimateableProperty.h"
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <memory>
#include <numpy/ndarrayobject.h>
using namespace aud;
extern PyObject* AUDError;
static PyObject* AnimateableProperty_new(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
AnimateablePropertyP* self = (AnimateablePropertyP*) type->tp_alloc(type, 0);
int count;
float value;
if(self != nullptr)
{
if(!PyArg_ParseTuple(args, "i|f:animateableProperty", &count, &value))
return nullptr;
try
{
if(PyTuple_Size(args) == 1)
{
self->animateableProperty = new std::shared_ptr<aud::AnimateableProperty>(new aud::AnimateableProperty(count));
}
else
{
self->animateableProperty = new std::shared_ptr<aud::AnimateableProperty>(new aud::AnimateableProperty(count, value));
}
}
catch(aud::Exception& e)
{
Py_DECREF(self);
PyErr_SetString(AUDError, e.what());
return nullptr;
}
}
return (PyObject*) self;
}
static void AnimateableProperty_dealloc(AnimateablePropertyP* self)
{
if(self->animateableProperty)
delete reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty);
Py_TYPE(self)->tp_free((PyObject*) self);
}
static PyObject* AnimateableProperty_read(AnimateablePropertyP* self, PyObject* args)
{
float position;
if(!PyArg_ParseTuple(args, "f", &position))
return nullptr;
int count = (*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->getCount();
npy_intp dims[1] = {count};
PyObject* np_array = PyArray_SimpleNew(1, dims, NPY_FLOAT32);
if(!np_array)
return nullptr;
float* out = static_cast<float*>(PyArray_DATA(reinterpret_cast<PyArrayObject*>(np_array)));
try
{
(*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->read(position, out);
return np_array;
}
catch(aud::Exception& e)
{
Py_DECREF(np_array);
PyErr_SetString(AUDError, e.what());
return nullptr;
}
}
PyDoc_STRVAR(M_aud_AnimateableProperty_read_doc, ".. method:: read(position)\n\n"
" Reads the properties value at the given position.\n\n"
" :param position: The position in the animation in frames.\n"
" :type position: float\n"
" :return: A numpy array of values representing the properties value.\n"
" :rtype: :class:`numpy.ndarray`\n");
static PyObject* AnimateableProperty_readSingle(AnimateablePropertyP* self, PyObject* args)
{
float position;
if(!PyArg_ParseTuple(args, "f", &position))
return nullptr;
try
{
float value = (*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->readSingle(position);
return Py_BuildValue("f", value);
}
catch(aud::Exception& e)
{
PyErr_SetString(AUDError, e.what());
return nullptr;
}
}
PyDoc_STRVAR(M_aud_AnimateableProperty_readSingle_doc, ".. method:: readSingle(position)\n\n"
" Reads the properties value at the given position, assuming there is exactly one value.\n\n"
" :param position: The position in the animation in frames.\n"
" :type position: float\n"
" :return: The value at that position.\n"
" :rtype: float\n\n");
static PyObject* AnimateableProperty_write(AnimateablePropertyP* self, PyObject* args)
{
PyObject* array_obj;
int position = -1;
if(!PyArg_ParseTuple(args, "O|i", &array_obj, &position))
return nullptr;
PyArrayObject* np_array = reinterpret_cast<PyArrayObject*>(PyArray_FROM_OTF(array_obj, NPY_FLOAT32, NPY_ARRAY_IN_ARRAY | NPY_ARRAY_FORCECAST));
if(!np_array)
{
PyErr_SetString(PyExc_TypeError, "data must be a numpy array of dtype float32");
return nullptr;
}
auto& prop = *reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty);
int prop_count = prop->getCount();
npy_intp size = PyArray_SIZE(np_array);
int ndim = PyArray_NDIM(np_array);
bool valid_shape = false;
// For 1D arrays, the total number of elements must be a multiple of the property count
if(ndim == 1)
{
valid_shape = (size % prop_count == 0);
}
// For 2D arrays, the number of elements in the second dimension must be the property count
else if(ndim == 2)
{
npy_intp* shape = PyArray_DIMS(np_array);
valid_shape = (shape[1] == prop_count);
}
if(!valid_shape)
{
PyErr_SetString(PyExc_ValueError, "array shape is invalid: must be 1D with length multiple of property count or 2D with the last dimension equal to property count");
Py_DECREF(np_array);
return nullptr;
}
int count = static_cast<int>(size / prop_count);
if(count < 1)
{
PyErr_SetString(PyExc_ValueError, "input array must have at least 1 element");
Py_DECREF(np_array);
return nullptr;
}
float* data_ptr = reinterpret_cast<float*>(PyArray_DATA(np_array));
try
{
if(position == -1)
{
if(count != 1)
{
PyErr_SetString(PyExc_ValueError, "input array must have exactly 1 element when position is not specified");
Py_DECREF(np_array);
return nullptr;
}
prop->write(data_ptr);
}
else
{
prop->write(data_ptr, position, count);
}
}
catch(aud::Exception& e)
{
PyErr_SetString(AUDError, e.what());
}
Py_DECREF(np_array);
Py_RETURN_NONE;
}
PyDoc_STRVAR(M_aud_AnimateableProperty_write_doc, ".. method:: write(data[, position])\n\n"
" Writes the properties value.\n\n"
" If `position` is also given, the property is marked animated and\n"
" the values are written starting at `position`.\n\n"
" :param data: numpy array of float32 values.\n"
" :type data: numpy.ndarray\n"
" :param position: The starting position in frames.\n"
" :type position: int\n\n");
static PyObject* AnimateableProperty_writeConstantRange(AnimateablePropertyP* self, PyObject* args)
{
PyObject* array_obj;
int position_start;
int position_end;
if(!PyArg_ParseTuple(args, "Oii", &array_obj, &position_start, &position_end))
return nullptr;
PyArrayObject* np_array = reinterpret_cast<PyArrayObject*>(PyArray_FROM_OTF(array_obj, NPY_FLOAT32, NPY_ARRAY_IN_ARRAY | NPY_ARRAY_FORCECAST));
if(!np_array)
{
PyErr_SetString(PyExc_TypeError, "data must be a numpy array of dtype float32");
return nullptr;
}
int ndim = PyArray_NDIM(np_array);
if(ndim != 1)
{
PyErr_SetString(PyExc_ValueError, "data must be a 1D numpy array");
Py_DECREF(np_array);
return nullptr;
}
float* data_ptr = reinterpret_cast<float*>(PyArray_DATA(np_array));
auto& prop = *reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty);
int prop_count = prop->getCount();
npy_intp size = PyArray_SIZE(np_array);
if(size != prop_count)
{
PyErr_Format(PyExc_ValueError, "input array length (%lld) does not match property count (%d)", size, prop_count);
Py_DECREF(np_array);
return nullptr;
}
try
{
prop->writeConstantRange(data_ptr, position_start, position_end);
}
catch(aud::Exception& e)
{
PyErr_SetString(AUDError, e.what());
return nullptr;
}
Py_DECREF(np_array);
Py_RETURN_NONE;
}
PyDoc_STRVAR(M_aud_AnimateableProperty_writeConstantRange_doc, ".. method:: writeConstantRange(data, position_start, position_end)\n\n"
" Fills the properties frame range with a constant value and marks it animated.\n\n"
" :param data: numpy array of float values representing the constant value.\n"
" :type data: numpy.ndarray\n"
" :param position_start: The start position in frames.\n"
" :type position_start: int\n"
" :param position_end: The end position in frames.\n"
" :type position_end: int\n\n");
static PyMethodDef AnimateableProperty_methods[] = {
{(char*) "read", (PyCFunction) AnimateableProperty_read, METH_VARARGS, M_aud_AnimateableProperty_read_doc},
{(char*) "readSingle", (PyCFunction) AnimateableProperty_readSingle, METH_VARARGS, M_aud_AnimateableProperty_readSingle_doc},
{(char*) "write", (PyCFunction) AnimateableProperty_write, METH_VARARGS, M_aud_AnimateableProperty_write_doc},
{(char*) "writeConstantRange", (PyCFunction) AnimateableProperty_writeConstantRange, METH_VARARGS, M_aud_AnimateableProperty_writeConstantRange_doc},
{nullptr} /* Sentinel */
};
static PyObject* AnimateableProperty_get_count(AnimateablePropertyP* self, void* nothing)
{
try
{
int count = (*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->getCount();
return Py_BuildValue("i", count);
}
catch(aud::Exception& e)
{
PyErr_SetString(AUDError, e.what());
return nullptr;
}
}
PyDoc_STRVAR(M_aud_AnimateableProperty_count_doc, "The count of floats for a property.");
static PyObject* AnimateableProperty_get_animated(AnimateablePropertyP* self, void* nothing)
{
try
{
bool animated = (*reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(self->animateableProperty))->isAnimated();
return PyBool_FromLong(animated);
}
catch(aud::Exception& e)
{
PyErr_SetString(AUDError, e.what());
return nullptr;
}
}
PyDoc_STRVAR(M_aud_AnimateableProperty_animated_doc, "Whether the property is animated.");
static PyGetSetDef AnimateableProperty_properties[] = {
{(char*) "count", (getter) AnimateableProperty_get_count, nullptr, M_aud_AnimateableProperty_count_doc, nullptr},
{(char*) "animated", (getter) AnimateableProperty_get_animated, nullptr, M_aud_AnimateableProperty_animated_doc, nullptr},
{nullptr} /* Sentinel */
};
PyDoc_STRVAR(M_aud_AnimateableProperty_doc, "An AnimateableProperty object stores an array of float values for animating sound properties (e.g. pan, volume, pitch-scale)");
// Note that AnimateablePropertyType name is already taken
PyTypeObject AnimateablePropertyPyType = {
PyVarObject_HEAD_INIT(nullptr, 0) "aud.AnimateableProperty", /* tp_name */
sizeof(AnimateablePropertyP), /* tp_basicsize */
0, /* tp_itemsize */
(destructor) AnimateableProperty_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
M_aud_AnimateableProperty_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
AnimateableProperty_methods, /* tp_methods */
0, /* tp_members */
AnimateableProperty_properties, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
AnimateableProperty_new, /* tp_new */
};
AUD_API PyObject* AnimateableProperty_empty()
{
return AnimateablePropertyPyType.tp_alloc(&AnimateablePropertyPyType, 0);
}
AUD_API AnimateablePropertyP* checkAnimateableProperty(PyObject* animateableProperty)
{
if(!PyObject_TypeCheck(animateableProperty, &AnimateablePropertyPyType))
{
PyErr_SetString(PyExc_TypeError, "Object is not of type AnimateableProperty!");
return nullptr;
}
return (AnimateablePropertyP*) animateableProperty;
}
bool initializeAnimateableProperty()
{
import_array1(false);
return PyType_Ready(&AnimateablePropertyPyType) >= 0;
}
void addAnimateablePropertyToModule(PyObject* module)
{
Py_INCREF(&AnimateablePropertyPyType);
PyModule_AddObject(module, "AnimateableProperty", (PyObject*) &AnimateablePropertyPyType);
}