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
This commit is contained in:
Jörg Müller
2025-08-23 17:25:38 +02:00
parent 334a66bf93
commit f085f88835
21 changed files with 1051 additions and 31 deletions

View File

@@ -27,7 +27,7 @@ The pipewire backend and many fixes have been provided by
- Sebastian Parborg
The rubberband integration (for time stretching and pitch scaling has been provided by
The rubberband integration (for time stretching and pitch scaling) has been provided by
- Kacey La

View File

@@ -803,10 +803,14 @@ if(WITH_RUBBERBAND)
set(RUBBERBAND_SRC
src/fx/TimeStretchPitchScale.cpp
src/fx/TimeStretchPitchScaleReader.cpp
src/fx/AnimateableTimeStretchPitchScale.cpp
src/fx/AnimateableTimeStretchPitchScaleReader.cpp
)
set(RUBBERBAND_HDR
include/fx/TimeStretchPitchScale.h
include/fx/TimeStretchPitchScaleReader.h
include/fx/AnimateableTimeStretchPitchScale.h
include/fx/AnimateableTimeStretchPitchScaleReader.h
)
add_definitions(-DWITH_RUBBERBAND)
@@ -1174,6 +1178,7 @@ endif()
if(WITH_PYTHON)
set(PYTHON_SRC
bindings/python/PyAnimateableProperty.cpp
bindings/python/PyAPI.cpp
bindings/python/PyDevice.cpp
bindings/python/PyDynamicMusic.cpp
@@ -1186,6 +1191,7 @@ if(WITH_PYTHON)
bindings/python/PyThreadPool.cpp
)
set(PYTHON_HDR
bindings/python/PyAnimateableProperty.h
bindings/python/PyAPI.h
bindings/python/PyDevice.h
bindings/python/PyDynamicMusic.h

View File

@@ -22,16 +22,6 @@
extern "C" {
#endif
/// Possible animatable properties for Sequence Factories and Entries.
typedef enum
{
AUD_AP_VOLUME,
AUD_AP_PANNING,
AUD_AP_PITCH,
AUD_AP_LOCATION,
AUD_AP_ORIENTATION
} AUD_AnimateablePropertyType;
/**
* Creates a new sequenced sound scene.
* \param fps The FPS of the scene.

View File

@@ -59,6 +59,7 @@
#ifdef WITH_RUBBERBAND
#include "fx/TimeStretchPitchScale.h"
#include "fx/AnimateableTimeStretchPitchScale.h"
#endif
#include <cassert>
@@ -795,7 +796,7 @@ AUD_API AUD_Sound* AUD_Sound_equalize(AUD_Sound* sound, float *definition, int s
#endif
#ifdef WITH_RUBBERBAND
AUD_API AUD_Sound* AUD_Sound_timeStretchPitchScale(AUD_Sound* sound, double timeRatio, double pitchScale, AUD_StretcherQuality quality, bool preserveFormant)
AUD_API AUD_Sound* AUD_Sound_timeStretchPitchScale(AUD_Sound* sound, double timeRatio, double pitchScale, AUD_StretcherQuality quality, char preserveFormant)
{
assert(sound);
try
@@ -807,4 +808,51 @@ AUD_API AUD_Sound* AUD_Sound_timeStretchPitchScale(AUD_Sound* sound, double time
return nullptr;
}
}
AUD_API AUD_Sound* AUD_Sound_animateableTimeStretchPitchScale(AUD_Sound* sound, float fps, double timeRatio, double pitchScale, AUD_StretcherQuality quality, char preserveFormant)
{
assert(sound);
try
{
return new AUD_Sound(new AnimateableTimeStretchPitchScale(*sound, fps, timeRatio, pitchScale, static_cast<StretcherQuality>(quality), preserveFormant));
}
catch(Exception&)
{
return nullptr;
}
}
AUD_API void AUD_Sound_animateableTimeStretchPitchScale_setConstantRangeAnimationData(AUD_Sound* sound, AUD_AnimateablePropertyType type, int frame_start, int frame_end,
float* data)
{
std::shared_ptr<AnimateableProperty> prop = std::dynamic_pointer_cast<AnimateableTimeStretchPitchScale>(*sound)->getAnimProperty(static_cast<AnimateablePropertyType>(type));
prop->writeConstantRange(data, frame_start, frame_end);
}
AUD_API void AUD_Sound_animateableTimeStretchPitchScale_setAnimationData(AUD_Sound* sound, AUD_AnimateablePropertyType type, int frame, float* data, char animated)
{
std::shared_ptr<AnimateableProperty> prop = std::dynamic_pointer_cast<AnimateableTimeStretchPitchScale>(*sound)->getAnimProperty(static_cast<AnimateablePropertyType>(type));
if(animated)
{
if(frame >= 0)
prop->write(data, frame, 1);
}
else
{
prop->write(data);
}
}
AUD_API float AUD_Sound_animateableTimeStretchPitchScale_getFPS(AUD_Sound* sound)
{
assert(sound);
return dynamic_cast<AnimateableTimeStretchPitchScale*>(sound->get())->getFPS();
}
AUD_API void AUD_Sound_animateableTimeStretchPitchScale_setFPS(AUD_Sound* sound, float value)
{
assert(sound);
dynamic_cast<AnimateableTimeStretchPitchScale*>(sound->get())->setFPS(value);
}
#endif

View File

@@ -419,7 +419,55 @@ extern AUD_API AUD_Sound* AUD_Sound_mutable(AUD_Sound* sound);
* \param preserveFormant Whether to preserve the vocal formants for the stretcher.
* \return A handle of the time-stretched, pitch scaled sound.
*/
extern AUD_API AUD_Sound* AUD_Sound_timeStretchPitchScale(AUD_Sound* sound, double timeRatio, double pitchScale, AUD_StretcherQuality quality, bool preserveFormant);
extern AUD_API AUD_Sound* AUD_Sound_timeStretchPitchScale(AUD_Sound* sound, double timeRatio, double pitchScale, AUD_StretcherQuality quality, char preserveFormant);
/**
* Time-stretches and pitch scales a sound with animation support
* \param sound The handle of the sound.
* \param fps The fps
* \param timeRatio The initial factor by which to stretch or compress time.
* \param pitchScale The initial factor by which to adjust the pitch.
* \param quality The processing quality level of the stretcher.
* \param preserveFormant Whether to preserve the vocal formants for the stretcher.
* \return A handle of the time-stretched, pitch scaled sound.
*/
extern AUD_API AUD_Sound* AUD_Sound_animateableTimeStretchPitchScale(AUD_Sound* sound, float fps, double timeRatio, double pitchScale, AUD_StretcherQuality quality,
char preserveFormant);
/**
* Writes animation data to the AnimatableTimeStretchPitchScale effect
* \param sequence The sound scene.
* \param type The type of animation data.
* \param frame_start Start of the frame range.
* \param frame_end End of the frame range.
* \param data The data to write.
*/
AUD_API void AUD_Sound_animateableTimeStretchPitchScale_setConstantRangeAnimationData(AUD_Sound* sound, AUD_AnimateablePropertyType type, int frame_start, int frame_end,
float* data);
/**
* Writes animation data to the AnimatableTimeStretchPitchScale effect
* \param entry The sequenced entry.
* \param type The type of animation data.
* \param frame The frame this data is for.
* \param data The data to write.
* \param animated Whether the attribute is animated.
*/
extern AUD_API void AUD_Sound_animateableTimeStretchPitchScale_setAnimationData(AUD_Sound* sound, AUD_AnimateablePropertyType type, int frame, float* data, char animated);
/**
* Sets the fps of an animated time-stretch, pitch-scaled sound.
* \param sound The sound to set the fps from.
* \param value The new fps to set.
*/
extern AUD_API void AUD_Sound_animateableTimeStretchPitchScale_setFPS(AUD_Sound* sound, AUD_AnimateablePropertyType type, int frame, float* data, char animated);
/**
* Retrieves the fps of an animated time-stretch, pitch-scaled sound.
* \param sequence The sound to get the fps from.
* \return The fps of the sound.
*/
extern AUD_API float AUD_Sound_animateableTimeStretchPitchScale_getFPS(AUD_Sound* sequence);
#endif
#ifdef __cplusplus

View File

@@ -210,3 +210,15 @@ typedef enum
AUD_STRETCHER_QUALITY_FAST = 1, /// Prioritize speed over audio quality
AUD_STRETCHER_QUALITY_CONSISTENT = 2 /// Prioritize consistency for dynamic pitch changes
} AUD_StretcherQuality;
/// Possible animatable properties for Sequence Factories and Entries.
typedef enum
{
AUD_AP_VOLUME,
AUD_AP_PANNING,
AUD_AP_PITCH,
AUD_AP_LOCATION,
AUD_AP_ORIENTATION,
AUD_AP_TIME_STRETCH,
AUD_AP_PITCH_SCALE
} AUD_AnimateablePropertyType;

View File

@@ -14,6 +14,7 @@
* limitations under the License.
******************************************************************************/
#include "PyAnimateableProperty.h"
#include "PyAPI.h"
#include "PySound.h"
#include "PyHandle.h"
@@ -104,6 +105,9 @@ PyInit_aud()
if(!initializeSource())
return nullptr;
if(!initializeAnimateableProperty())
return nullptr;
#ifdef WITH_CONVOLUTION
if(!initializeImpulseResponse())
return nullptr;
@@ -116,6 +120,7 @@ PyInit_aud()
if(module == nullptr)
return nullptr;
addAnimateablePropertyToModule(module);
addSoundToModule(module);
addHandleToModule(module);
addDeviceToModule(module);
@@ -141,6 +146,8 @@ PyInit_aud()
PY_MODULE_ADD_CONSTANT(module, AP_PITCH);
PY_MODULE_ADD_CONSTANT(module, AP_LOCATION);
PY_MODULE_ADD_CONSTANT(module, AP_ORIENTATION);
PY_MODULE_ADD_CONSTANT(module, AP_TIME_STRETCH);
PY_MODULE_ADD_CONSTANT(module, AP_PITCH_SCALE);
// channels constants
PY_MODULE_ADD_CONSTANT(module, CHANNELS_INVALID);
PY_MODULE_ADD_CONSTANT(module, CHANNELS_MONO);

View File

@@ -0,0 +1,398 @@
/*******************************************************************************
* 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);
}

View File

@@ -0,0 +1,34 @@
/*******************************************************************************
* 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.
******************************************************************************/
#pragma once
#include <Python.h>
#include "Audaspace.h"
typedef void Reference_AnimateableProperty;
typedef struct
{
PyObject_HEAD Reference_AnimateableProperty* animateableProperty;
} AnimateablePropertyP;
extern AUD_API PyObject* AnimateableProperty_empty();
extern AUD_API AnimateablePropertyP* checkAnimateableProperty(PyObject* animateableProperty);
bool initializeAnimateableProperty();
void addAnimateablePropertyToModule(PyObject* module);

View File

@@ -98,7 +98,7 @@ PyDoc_STRVAR(M_aud_DynamicMusic_addTransition_doc,
" :type end: int\n"
" :arg transition: The transition sound.\n"
" :type transition: :class:`Sound`\n"
" :return: false if the ini or end scenes don't exist, true othrwise.\n"
" :return: false if the ini or end scenes don't exist, true otherwise.\n"
" :rtype: bool");
static PyObject *

View File

@@ -189,7 +189,7 @@ PyDoc_STRVAR(M_aud_PlaybackManager_get_volume_doc,
" Retrieves the volume of a category.\n\n"
" :arg catKey: the key of the category.\n"
" :type catKey: int\n"
" :return: The volume of the cateogry.\n"
" :return: The volume of the category.\n"
" :rtype: float\n\n");
static PyObject *

View File

@@ -14,6 +14,7 @@
* limitations under the License.
******************************************************************************/
#include "PyAnimateableProperty.h"
#include "PySound.h"
#include "PySource.h"
#include "PyThreadPool.h"
@@ -66,6 +67,7 @@
#ifdef WITH_RUBBERBAND
#include "fx/AnimateableTimeStretchPitchScale.h"
#include "fx/TimeStretchPitchScale.h"
#endif
@@ -1794,10 +1796,10 @@ Sound_binaural(Sound* self, PyObject* args)
#ifdef WITH_RUBBERBAND
PyDoc_STRVAR(M_aud_Sound_timeStretchPitchScale_doc, ".. method:: timeStretchPitchScale(time_ratio, pitch_scale, quality, preserve_formant)\n\n"
PyDoc_STRVAR(M_aud_Sound_timeStretchPitchScale_doc, ".. method:: timeStretchPitchScale(time_stretch, pitch_scale, quality, preserve_formant)\n\n"
" Applies time-stretching and pitch-scaling to the sound.\n\n"
" :arg time_ratio: The factor by which to stretch or compress time.\n"
" :type time_ratio: float\n"
" :arg time_stretch: The factor by which to stretch or compress time.\n"
" :type time_stretch: float\n"
" :arg pitch_scale: The factor by which to adjust the pitch.\n"
" :type pitch_scale: float\n"
" :arg quality: Rubberband stretcher quality (STRETCHER_QUALITY_*).\n"
@@ -1806,18 +1808,22 @@ PyDoc_STRVAR(M_aud_Sound_timeStretchPitchScale_doc, ".. method:: timeStretchPitc
" :type preserve_formant: bool\n"
" :return: The created :class:`Sound` object.\n"
" :rtype: :class:`Sound`");
static PyObject *
Sound_timeStretchPitchScale(Sound* self, PyObject* args)
static PyObject* Sound_timeStretchPitchScale(Sound* self, PyObject* args, PyObject* kwds)
{
double time_ratio, pitch_scale;
double time_stretch = 1.0;
double pitch_scale = 1.0;
int quality = 0;
int preserve_formant = 0;
if(!PyArg_ParseTuple(args, "dd|i|p:timeStretchPitchScale", &time_ratio, &pitch_scale, &quality, &preserve_formant))
static const char* kwlist[] = {"time_stretch", "pitch_scale", "quality", "preserve_formant", nullptr};
if(!PyArg_ParseTupleAndKeywords(args, kwds, "|ddip:timeStretchPitchScale", const_cast<char**>(kwlist), &time_stretch, &pitch_scale, &quality, &preserve_formant))
{
return nullptr;
}
if(quality < 0 || quality > 2)
{
PyErr_WarnEx(PyExc_UserWarning, "Invalid quality value: using default (0 = HIGH)", 1);
PyErr_WarnEx(PyExc_UserWarning, "Invalid quality value: using default (0 = STRETCHER_QUALITY_HIGH)", 1);
quality = 0;
}
@@ -1828,7 +1834,7 @@ Sound_timeStretchPitchScale(Sound* self, PyObject* args)
{
try
{
parent->sound = new std::shared_ptr<ISound>(new TimeStretchPitchScale(*reinterpret_cast<std::shared_ptr<ISound>*>(self->sound), time_ratio, pitch_scale,
parent->sound = new std::shared_ptr<ISound>(new TimeStretchPitchScale(*reinterpret_cast<std::shared_ptr<ISound>*>(self->sound), time_stretch, pitch_scale,
static_cast<StretcherQuality>(quality), preserve_formant != 0));
}
catch(Exception& e)
@@ -1842,7 +1848,109 @@ Sound_timeStretchPitchScale(Sound* self, PyObject* args)
return (PyObject*)parent;
}
PyDoc_STRVAR(M_aud_Sound_animateableTimeStretchPitchScale_doc, ".. method:: animateableTimeStretchPitchScale(fps[, time_stretch, pitch_scale, quality, preserve_formant])\n\n"
" Applies time-stretching and pitch-scaling to the sound.\n\n"
" :arg fps: The FPS of the animation system.\n"
" :type fps float\n"
" :arg time_stretch: The factor by which to stretch or compress time.\n"
" :type time_stretch: float or :class:`AnimateablePropertyP`\n"
" :arg pitch_scale: The factor by which to adjust the pitch.\n"
" :type pitch_scale: float or :class:`AnimateablePropertyP`\n "
" :arg quality: Rubberband stretcher quality (STRETCHER_QUALITY_*).\n"
" :type quality: int\n"
" :arg preserve_formant: Whether to preserve the vocal formants during pitch-shifting.\n"
" :type preserve_formant: bool\n"
" :return: The created :class:`Sound` object.\n"
" :rtype: :class:`Sound`");
static PyObject* Sound_animateableTimeStretchPitchScale(Sound* self, PyObject* args, PyObject* kwds)
{
float fps;
PyObject* object1 = Py_None;
PyObject* object2 = Py_None;
int quality = 0;
int preserve_formant = 0;
static const char* kwlist[] = {"fps", "time_stretch", "pitch_scale", "quality", "preserve_formant", nullptr};
if(!PyArg_ParseTupleAndKeywords(args, kwds, "f|OOip:animateableTimeStretchPitchScale", const_cast<char**>(kwlist), &fps, &object1, &object2, &quality, &preserve_formant))
{
return nullptr;
}
std::shared_ptr<aud::AnimateableProperty> time_stretch;
std::shared_ptr<aud::AnimateableProperty> pitch_scale;
if(fps <= 0)
{
PyErr_SetString(PyExc_ValueError, "FPS must be greater 0!");
return nullptr;
}
if(object1 == Py_None)
{
time_stretch = std::make_shared<aud::AnimateableProperty>(1, 1.0);
}
else if(PyNumber_Check(object1))
{
time_stretch = std::make_shared<aud::AnimateableProperty>(1, PyFloat_AsDouble(object1));
}
else
{
AnimateablePropertyP* time_stretch_prop = checkAnimateableProperty(object1);
if(!time_stretch_prop)
{
return nullptr;
}
time_stretch = *reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(time_stretch_prop->animateableProperty);
}
if(object2 == Py_None)
{
pitch_scale = std::make_shared<aud::AnimateableProperty>(1, 1.0);
}
else if(PyNumber_Check(object2))
{
pitch_scale = std::make_shared<aud::AnimateableProperty>(1, PyFloat_AsDouble(object2));
}
else
{
AnimateablePropertyP* pitch_scale_prop = checkAnimateableProperty(object2);
if(!pitch_scale_prop)
{
return nullptr;
}
pitch_scale = *reinterpret_cast<std::shared_ptr<aud::AnimateableProperty>*>(pitch_scale_prop->animateableProperty);
}
if(quality < 0 || quality > 2)
{
PyErr_WarnEx(PyExc_UserWarning, "Invalid quality value: using default (0 = STRETCHER_QUALITY_HIGH)", 1);
quality = 0;
}
PyTypeObject* type = Py_TYPE(self);
Sound* parent = (Sound*) type->tp_alloc(type, 0);
if(parent != nullptr)
{
try
{
parent->sound = new std::shared_ptr<ISound>(new AnimateableTimeStretchPitchScale(*reinterpret_cast<std::shared_ptr<ISound>*>(self->sound), fps, time_stretch,
pitch_scale, static_cast<StretcherQuality>(quality), preserve_formant != 0));
}
catch(Exception& e)
{
Py_DECREF(parent);
PyErr_SetString(AUDError, e.what());
return nullptr;
}
}
return (PyObject*) parent;
}
#endif
static PyMethodDef Sound_methods[] = {
{"data", (PyCFunction)Sound_data, METH_NOARGS,
M_aud_Sound_data_doc
@@ -1958,9 +2066,10 @@ static PyMethodDef Sound_methods[] = {
},
#endif
#ifdef WITH_RUBBERBAND
{"timeStretchPitchScale", (PyCFunction)Sound_timeStretchPitchScale, METH_VARARGS,
M_aud_Sound_timeStretchPitchScale_doc
},
{"timeStretchPitchScale", (PyCFunction)Sound_timeStretchPitchScale, METH_VARARGS | METH_KEYWORDS,
M_aud_Sound_timeStretchPitchScale_doc},
{"animateableTimeStretchPitchScale", (PyCFunction) Sound_animateableTimeStretchPitchScale, METH_VARARGS | METH_KEYWORDS,
M_aud_Sound_animateableTimeStretchPitchScale_doc},
#endif
{nullptr} /* Sentinel */
};

View File

@@ -36,6 +36,14 @@ if sys.platform == 'win32':
else:
extra_args.append('-std=c++17')
macros = []
if '@WITH_FFTW@' == 'ON':
macros.append(('WITH_CONVOLUTION', None))
if '@WITH_RUBBERBAND@' == 'ON':
macros.append(('WITH_RUBBERBAND', None))
audaspace = Extension(
'aud',
include_dirs = ['@CMAKE_CURRENT_BINARY_DIR@', os.path.join(source_directory, '../../include'), numpy.get_include()] + (['@FFTW_INCLUDE_DIR@'] if '@WITH_FFTW@' == 'ON' else []),
@@ -43,8 +51,8 @@ audaspace = Extension(
library_dirs = ['.', 'Release', 'Debug'],
language = 'c++',
extra_compile_args = extra_args,
define_macros = [('WITH_CONVOLUTION', None)] if '@WITH_FFTW@' == 'ON' else [],
sources = [os.path.join(source_directory, file) for file in ['PyAPI.cpp', 'PyDevice.cpp', 'PyHandle.cpp', 'PySound.cpp', 'PySequenceEntry.cpp', 'PySequence.cpp', 'PyPlaybackManager.cpp', 'PyDynamicMusic.cpp', 'PyThreadPool.cpp', 'PySource.cpp'] + (['PyImpulseResponse.cpp', 'PyHRTF.cpp'] if '@WITH_FFTW@' == 'ON' else [])]
define_macros = macros,
sources = [os.path.join(source_directory, file) for file in ['PyAnimateableProperty.cpp', 'PyAPI.cpp', 'PyDevice.cpp', 'PyHandle.cpp', 'PySound.cpp', 'PySequenceEntry.cpp', 'PySequence.cpp', 'PyPlaybackManager.cpp', 'PyDynamicMusic.cpp', 'PyThreadPool.cpp', 'PySource.cpp'] + (['PyImpulseResponse.cpp', 'PyHRTF.cpp'] if '@WITH_FFTW@' == 'ON' else [])]
)
setup(
@@ -57,6 +65,6 @@ setup(
license = 'Apache License 2.0',
long_description = codecs.open(os.path.join(source_directory, '../../README.md'), 'r', 'utf-8').read(),
ext_modules = [audaspace],
headers = [os.path.join(source_directory, file) for file in ['PyAPI.h', 'PyDevice.h', 'PyHandle.h', 'PySound.h', 'PySequenceEntry.h', 'PySequence.h', 'PyPlaybackManager.h', 'PyDynamicMusic.h', 'PyThreadPool.h', 'PySource.h'] + (['PyImpulseResponse.h', 'PyHRTF.h'] if '@WITH_FFTW@' == 'ON' else [])] + ['Audaspace.h']
headers = [os.path.join(source_directory, file) for file in ['PyAnimateableProperty.h', 'PyAPI.h', 'PyDevice.h', 'PyHandle.h', 'PySound.h', 'PySequenceEntry.h', 'PySequence.h', 'PyPlaybackManager.h', 'PyDynamicMusic.h', 'PyThreadPool.h', 'PySource.h'] + (['PyImpulseResponse.h', 'PyHRTF.h'] if '@WITH_FFTW@' == 'ON' else [])] + ['Audaspace.h']
)

View File

@@ -0,0 +1,123 @@
/*******************************************************************************
* 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.
******************************************************************************/
#pragma once
/**
* @file AnimateableTimeStretchPitchScale.h
* @ingroup fx
* The AnimateableTimeStretchPitchScale class.
*/
#include "fx/Effect.h"
#include "fx/TimeStretchPitchScale.h"
#include "sequence/AnimateableProperty.h"
AUD_NAMESPACE_BEGIN
/**
* This sound allows a sound to be time-stretched and pitch scaled with animation support
* \note The reader has to be seekable.
*/
class AUD_API AnimateableTimeStretchPitchScale : public Effect
{
private:
/**
* The FPS of the animation system.
*/
float m_fps;
/**
* The animateable time-stretch property.
*/
std::shared_ptr<AnimateableProperty> m_timeStretch;
/**
* The animateable pitch-scale property.
*/
std::shared_ptr<AnimateableProperty> m_pitchScale;
/**
* Rubberband stretcher quality options.
*/
StretcherQuality m_quality;
/**
* Whether to preserve the vocal formants for the stretcher.
*/
bool m_preserveFormant;
// delete copy constructor and operator=
AnimateableTimeStretchPitchScale(const AnimateableTimeStretchPitchScale&) = delete;
AnimateableTimeStretchPitchScale& operator=(const AnimateableTimeStretchPitchScale&) = delete;
public:
/**
* Creates a new time-stretch, pitch-scaled sound that can be animated.
* \param sound The input sound.
* \param fps The fps of the animation system.
* \param timeRatio The starting factor by which to stretch or compress time.
* \param pitchScale The starting factor by which to adjust the pitch.
* \param quality The processing quality level of the stretcher.
* \param preserveFormant Whether to preserve the vocal formants for the stretcher.
*/
AnimateableTimeStretchPitchScale(std::shared_ptr<ISound> sound, float fps, float timeStretch, float pitchScale, StretcherQuality quality, bool preserveFormant);
/**
* Creates a new time-stretch, pitch-scaled sound that can be animated.
* \param sound The input sound.
* \param fps The fps of the anumation system.
* \param timeRatio The animateable time-stretch property.
* \param pitchScale The animateable pitch-scale property.
* \param quality The processing quality level of the stretcher.
* \param preserveFormant Whether to preserve the vocal formants for the stretcher.
*/
AnimateableTimeStretchPitchScale(std::shared_ptr<ISound> sound, float fps, std::shared_ptr<AnimateableProperty> timeStretch, std::shared_ptr<AnimateableProperty> pitchScale,
StretcherQuality quality, bool preserveFormant);
/**
* Returns whether formant preservation is enabled.
*/
bool getPreserveFormant() const;
/**
* Returns the quality of the stretcher.
*/
StretcherQuality getStretcherQuality() const;
/**
* Retrieves one of the animated properties of the sound.
* \param type Which animated property to retrieve.
* \return A shared pointer to the animated property
*/
std::shared_ptr<AnimateableProperty> getAnimProperty(AnimateablePropertyType type);
/**
* Retrieves the animation system's FPS.
* \return The animation system's FPS.
*/
float getFPS() const;
/**
* Sets the animation system's FPS.
* \param fps The new FPS.
*/
void setFPS(float fps);
virtual std::shared_ptr<IReader> createReader();
};
AUD_NAMESPACE_END

View File

@@ -0,0 +1,72 @@
/*******************************************************************************
* 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.
******************************************************************************/
#pragma once
/**
* @file AnimateableTimeStretchPitchScaleReader.h
* @ingroup fx
* The AnimateableTimeStretchPitchScaleReader class.
*/
#include "fx/AnimateableTimeStretchPitchScale.h"
#include "fx/TimeStretchPitchScaleReader.h"
AUD_NAMESPACE_BEGIN
/**
* This class reads from another reader and applies time-stretching and pitch scaling with support for animating both properties.
*/
class AUD_API AnimateableTimeStretchPitchScaleReader : public TimeStretchPitchScaleReader
{
private:
/**
* The FPS of the animation system.
*/
float m_fps;
/**
* The animateable time-stretch property.
*/
std::shared_ptr<AnimateableProperty> m_timeStretch;
/**
* The animateable pitch-scale property.
*/
std::shared_ptr<AnimateableProperty> m_pitchScale;
// delete copy constructor and operator=
AnimateableTimeStretchPitchScaleReader(const AnimateableTimeStretchPitchScaleReader&) = delete;
AnimateableTimeStretchPitchScaleReader& operator=(const AnimateableTimeStretchPitchScaleReader&) = delete;
public:
/**
* Creates a new animateable time-stretch, pitch scale reader.
* \param reader The input reader.
* \param fps The FPS of the animation system.
* \param timeStretch The animateable time-stretch property.
* \param pitchScale The animateable pitch-scale property.
* \param quality The stretcher quality options.
* \param preserveFormant Whether to preserve vocal formants.
*/
AnimateableTimeStretchPitchScaleReader(std::shared_ptr<IReader> reader, float fp, std::shared_ptr<AnimateableProperty> timeStretch,
std::shared_ptr<AnimateableProperty> pitchScale, StretcherQuality quality, bool preserveFormant);
virtual void read(int& length, bool& eos, sample_t* buffer) override;
virtual void seek(int position) override;
};
AUD_NAMESPACE_END

View File

@@ -89,6 +89,12 @@ public:
* Returns whether formant preservation is enabled.
*/
bool getPreserveFormant() const;
/**
* Returns the quality of the stretcher.
*/
StretcherQuality getStretcherQuality() const;
virtual std::shared_ptr<IReader> createReader();
};

View File

@@ -37,7 +37,9 @@ enum AnimateablePropertyType
AP_PANNING,
AP_PITCH,
AP_LOCATION,
AP_ORIENTATION
AP_ORIENTATION,
AP_TIME_STRETCH,
AP_PITCH_SCALE
};
/**
@@ -127,6 +129,13 @@ public:
*/
void read(float position, float* out);
/**
* Reads the property's value at the specified position, assuming there is exactly one value
* \param position The position in the animation in frames.
* \return The value at the position.
*/
float readSingle(float position);
/**
* Returns whether the property is animated.
* \return Whether the property is animated.

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* 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 "fx/AnimateableTimeStretchPitchScale.h"
#include "fx/AnimateableTimeStretchPitchScaleReader.h"
AUD_NAMESPACE_BEGIN
AnimateableTimeStretchPitchScale::AnimateableTimeStretchPitchScale(std::shared_ptr<ISound> sound, float fps, float timeStretch, float pitchScale, StretcherQuality quality,
bool preserveFormant) :
Effect(sound),
m_fps(fps),
m_timeStretch(std::make_shared<AnimateableProperty>(1, timeStretch)),
m_pitchScale(std::make_shared<AnimateableProperty>(1, pitchScale)),
m_quality(quality),
m_preserveFormant(preserveFormant)
{
}
AnimateableTimeStretchPitchScale::AnimateableTimeStretchPitchScale(std::shared_ptr<ISound> sound, float fps, std::shared_ptr<AnimateableProperty> timeStretch,
std::shared_ptr<AnimateableProperty> pitchScale, StretcherQuality quality, bool preserveFormant) :
Effect(sound), m_fps(fps), m_timeStretch(timeStretch), m_pitchScale(pitchScale), m_quality(quality), m_preserveFormant(preserveFormant)
{
}
std::shared_ptr<IReader> AnimateableTimeStretchPitchScale::createReader()
{
return std::make_shared<AnimateableTimeStretchPitchScaleReader>(getReader(), m_fps, m_timeStretch, m_pitchScale, m_quality, m_preserveFormant);
}
bool AnimateableTimeStretchPitchScale::getPreserveFormant() const
{
return m_preserveFormant;
}
StretcherQuality AnimateableTimeStretchPitchScale::getStretcherQuality() const
{
return m_quality;
}
std::shared_ptr<AnimateableProperty> AnimateableTimeStretchPitchScale::getAnimProperty(AnimateablePropertyType type)
{
switch(type)
{
case AP_TIME_STRETCH:
return m_timeStretch;
case AP_PITCH_SCALE:
return m_pitchScale;
default:
return nullptr;
}
}
float AnimateableTimeStretchPitchScale::getFPS() const
{
return m_fps;
}
void AnimateableTimeStretchPitchScale::setFPS(float fps)
{
m_fps = fps;
}
AUD_NAMESPACE_END

View File

@@ -0,0 +1,58 @@
/*******************************************************************************
* 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 "fx/AnimateableTimeStretchPitchScaleReader.h"
#include "IReader.h"
AUD_NAMESPACE_BEGIN
AnimateableTimeStretchPitchScaleReader::AnimateableTimeStretchPitchScaleReader(std::shared_ptr<IReader> reader, float fps, std::shared_ptr<AnimateableProperty> timeStretch,
std::shared_ptr<AnimateableProperty> pitchScale, StretcherQuality quality, bool preserveFormant) :
TimeStretchPitchScaleReader(reader, timeStretch->readSingle(0), pitchScale->readSingle(0), quality, preserveFormant),
m_fps(fps),
m_timeStretch(timeStretch),
m_pitchScale(pitchScale)
{
}
void AnimateableTimeStretchPitchScaleReader::read(int& length, bool& eos, sample_t* buffer)
{
int position = getPosition();
double time = double(position) / double(m_reader->getSpecs().rate);
float frame = time * m_fps;
float timeRatio = m_timeStretch->readSingle(frame);
setTimeRatio(timeRatio);
float pitchScale = m_pitchScale->readSingle(frame);
setPitchScale(pitchScale);
TimeStretchPitchScaleReader::read(length, eos, buffer);
}
void AnimateableTimeStretchPitchScaleReader::seek(int position)
{
double time = double(position) / double(m_reader->getSpecs().rate);
float frame = time * m_fps;
float timeRatio = m_timeStretch->readSingle(frame);
setTimeRatio(timeRatio);
float pitchScale = m_pitchScale->readSingle(frame);
setPitchScale(pitchScale);
TimeStretchPitchScaleReader::seek(position);
}
AUD_NAMESPACE_END

View File

@@ -45,4 +45,9 @@ bool TimeStretchPitchScale::getPreserveFormant() const
{
return m_preserveFormant;
}
StretcherQuality TimeStretchPitchScale::getStretcherQuality() const
{
return m_quality;
}
AUD_NAMESPACE_END

View File

@@ -16,6 +16,7 @@
#include "sequence/AnimateableProperty.h"
#include <cassert>
#include <cstring>
#include <cmath>
#include <mutex>
@@ -228,6 +229,14 @@ void AnimateableProperty::read(float position, float* out)
}
}
float AnimateableProperty::readSingle(float position)
{
assert(m_count == 1);
float value;
read(position, &value);
return value;
}
bool AnimateableProperty::isAnimated() const
{
return m_isAnimated;