From 957ee8953b8d10b5f359504edfb89c1809e06d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20M=C3=BCller?= Date: Sat, 26 Jul 2025 19:00:34 +0200 Subject: [PATCH] Audaspace: porting changes from upstream. This change introduces time stretching and pitch scaling. However, none of this will be compiled at the moment, as rubberband needs to be added to the dependencies first. Credit: Kacey La --- extern/audaspace/AUTHORS | 4 + extern/audaspace/CHANGES | 25 +++ extern/audaspace/CMakeLists.txt | 36 ++- extern/audaspace/bindings/C/AUD_Sound.cpp | 19 ++ extern/audaspace/bindings/C/AUD_Sound.h | 13 ++ extern/audaspace/bindings/C/AUD_Types.h | 10 + extern/audaspace/bindings/python/PyAPI.cpp | 12 +- extern/audaspace/bindings/python/PySound.cpp | 73 +++++- extern/audaspace/blender_config.cmake | 1 + .../include/fx/TimeStretchPitchScale.h | 95 ++++++++ .../include/fx/TimeStretchPitchScaleReader.h | 125 +++++++++++ .../src/fx/TimeStretchPitchScale.cpp | 48 ++++ .../src/fx/TimeStretchPitchScaleReader.cpp | 208 ++++++++++++++++++ 13 files changed, 661 insertions(+), 8 deletions(-) create mode 100644 extern/audaspace/include/fx/TimeStretchPitchScale.h create mode 100644 extern/audaspace/include/fx/TimeStretchPitchScaleReader.h create mode 100644 extern/audaspace/src/fx/TimeStretchPitchScale.cpp create mode 100644 extern/audaspace/src/fx/TimeStretchPitchScaleReader.cpp diff --git a/extern/audaspace/AUTHORS b/extern/audaspace/AUTHORS index 627911654e4..6cec1a1697c 100644 --- a/extern/audaspace/AUTHORS +++ b/extern/audaspace/AUTHORS @@ -27,6 +27,10 @@ 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 + +- Kacey La + Several people provided fixes: - Aaron Carlisle diff --git a/extern/audaspace/CHANGES b/extern/audaspace/CHANGES index ff75d0b3574..80b11d95da2 100644 --- a/extern/audaspace/CHANGES +++ b/extern/audaspace/CHANGES @@ -1,3 +1,28 @@ +Audaspace 1.7 + +- Rewrite of the Synchronizer API. +- Unified backends with a mixing thread (Jack, Pulse, Pipewire, CoreAudio) +- Various fixes for CoreAudio. + +Detailed list of changes: + +46036fa Bugfix for MixingThreadDevice. +bdd4ba5 Reintroducing ThreadedDevice. +eabbeee CoreAudio: remove OpenCloseDevice functionality. +a746d92 CoreAudio fixes. +0cbd14e Fixes. +82585f8 CoreAudioDevice/OpenCloseDevice: base on MixingThreadDevice. +a6341e7 Adding MixingThreadDevice and refactor deriving classes. +d2de831 Refactor PipeWireDevice and use own RingBuffer class. +78291ef Jack: use own RingBuffer class. +cbf62c2 CoreAudio: compile fixes. +caafed6 Remove resumeOnSync API again since no device supports it properly. +22f9c8d Modifying Jack transport sync callback behavior. +8cf2429 Remove sync callbacks for own calls. +c070757 Add resumeOnSync method for precise synchronization with Jack. +ce417e5 Finish synchronizer refactor. +fbec554 First step towards handle independent synchronizer. + Audaspace 1.6 - Pipewire support (thanks Sebastian Parborg!) diff --git a/extern/audaspace/CMakeLists.txt b/extern/audaspace/CMakeLists.txt index c17a03aeaca..4333ffbf74c 100644 --- a/extern/audaspace/CMakeLists.txt +++ b/extern/audaspace/CMakeLists.txt @@ -23,7 +23,7 @@ endif() project(audaspace) -set(AUDASPACE_VERSION 1.6) +set(AUDASPACE_VERSION 1.7) set(AUDASPACE_LONG_VERSION ${AUDASPACE_VERSION}.0) if(DEFINED AUDASPACE_CMAKE_CFG) @@ -288,6 +288,7 @@ if(AUDASPACE_STANDALONE) option(WITH_LIBSNDFILE "Build With LibSndFile" TRUE) option(WITH_OPENAL "Build With OpenAL" TRUE) option(WITH_PYTHON "Build With Python Library" TRUE) + option(WITH_RUBBERBAND "Build With Rubber Band Library" TRUE) option(WITH_SDL "Build With SDL" TRUE) option(WITH_STRICT_DEPENDENCIES "Error and abort instead of warning if a library is not found." FALSE) if(APPLE) @@ -792,6 +793,39 @@ if(WITH_PYTHON) endif() endif() +# Rubber Band Library +if(WITH_RUBBERBAND) + if(AUDASPACE_STANDALONE) + find_package(Rubberband ${PACKAGE_OPTION}) + endif() + + if(RUBBERBAND_FOUND) + set(RUBBERBAND_SRC + src/fx/TimeStretchPitchScale.cpp + src/fx/TimeStretchPitchScaleReader.cpp + ) + set(RUBBERBAND_HDR + include/fx/TimeStretchPitchScale.h + include/fx/TimeStretchPitchScaleReader.h + ) + + add_definitions(-DWITH_RUBBERBAND) + + list(APPEND INCLUDE ${RUBBERBAND_INCLUDE_DIRS}) + list(APPEND LIBRARIES ${RUBBERBAND_LIBRARIES}) + + list(APPEND SRC ${RUBBERBAND_SRC}) + list(APPEND HDR ${RUBBERBAND_HDR}) + else() + if(AUDASPACE_STANDALONE) + set(WITH_RUBBERBAND FALSE CACHE BOOL "Build with Rubber Band" FORCE) + else() + set(WITH_RUBBERBAND FALSE) + endif() + message(WARNING "Rubber Band Library not found, time-stretching and pitch-scaling functionality will not be built.") + endif() +endif() + # SDL if(WITH_SDL) if(AUDASPACE_STANDALONE) diff --git a/extern/audaspace/bindings/C/AUD_Sound.cpp b/extern/audaspace/bindings/C/AUD_Sound.cpp index 8398f9c5a6f..cb3f56b39e8 100644 --- a/extern/audaspace/bindings/C/AUD_Sound.cpp +++ b/extern/audaspace/bindings/C/AUD_Sound.cpp @@ -57,6 +57,10 @@ #include "fx/Equalizer.h" #endif +#ifdef WITH_RUBBERBAND +#include "fx/TimeStretchPitchScale.h" +#endif + #include #include @@ -789,3 +793,18 @@ 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) +{ + assert(sound); + try + { + return new AUD_Sound(new TimeStretchPitchScale(*sound, timeRatio, pitchScale, static_cast(quality), preserveFormant)); + } + catch(Exception&) + { + return nullptr; + } +} +#endif diff --git a/extern/audaspace/bindings/C/AUD_Sound.h b/extern/audaspace/bindings/C/AUD_Sound.h index e8a41e8ae7b..d28e66b51c4 100644 --- a/extern/audaspace/bindings/C/AUD_Sound.h +++ b/extern/audaspace/bindings/C/AUD_Sound.h @@ -409,6 +409,19 @@ extern AUD_API AUD_Sound* AUD_Sound_mutable(AUD_Sound* sound); extern AUD_API AUD_Sound* AUD_Sound_equalize(AUD_Sound* sound, float *definition, int size, float maxFreqEq, int sizeConversion); #endif +#ifdef WITH_RUBBERBAND + /** + * Time-stretches and pitch scales a sound. + * \param sound The handle of the sound. + * \param timeRatio The factor by which to stretch or compress time. + * \param pitchScale The 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_timeStretchPitchScale(AUD_Sound* sound, double timeRatio, double pitchScale, AUD_StretcherQuality quality, bool preserveFormant); +#endif + #ifdef __cplusplus } #endif diff --git a/extern/audaspace/bindings/C/AUD_Types.h b/extern/audaspace/bindings/C/AUD_Types.h index 8384273e9ae..d64481abc3a 100644 --- a/extern/audaspace/bindings/C/AUD_Types.h +++ b/extern/audaspace/bindings/C/AUD_Types.h @@ -200,3 +200,13 @@ typedef struct /// Audio data parameters. AUD_DeviceSpecs specs; } AUD_StreamInfo; + +/** + * The Rubber Band stretcher quality. + */ +typedef enum +{ + AUD_STRETCHER_QUALITY_HIGH = 0, /// Prioritize high-quality pitch processing + AUD_STRETCHER_QUALITY_FAST = 1, /// Prioritize speed over audio quality + AUD_STRETCHER_QUALITY_CONSISTENT = 2 /// Prioritize consistency for dynamic pitch changes +} AUD_StretcherQuality; diff --git a/extern/audaspace/bindings/python/PyAPI.cpp b/extern/audaspace/bindings/python/PyAPI.cpp index 16c7b662563..3d18e20c2b2 100644 --- a/extern/audaspace/bindings/python/PyAPI.cpp +++ b/extern/audaspace/bindings/python/PyAPI.cpp @@ -30,6 +30,10 @@ #include "PyHRTF.h" #endif +#ifdef WITH_RUBBERBAND +#include "fx/TimeStretchPitchScale.h" +#endif + #include "respec/Specification.h" #include "devices/IHandle.h" #include "devices/I3DDevice.h" @@ -43,7 +47,6 @@ #include using namespace aud; - // ==================================================================== #define PY_MODULE_ADD_CONSTANT(module, name) PyModule_AddIntConstant(module, #name, name) @@ -203,6 +206,13 @@ PyInit_aud() PY_MODULE_ADD_CONSTANT(module, STATUS_PLAYING); PY_MODULE_ADD_CONSTANT(module, STATUS_STOPPED); +#ifdef WITH_RUBBERBAND + // stretcher quality + PyModule_AddIntConstant(module, "STRETCHER_QUALITY_HIGH", static_cast(StretcherQuality::HIGH)); + PyModule_AddIntConstant(module, "STRETCHER_QUALITY_FAST", static_cast(StretcherQuality::FAST)); + PyModule_AddIntConstant(module, "STRETCHER_QUALITY_CONSISTENT", static_cast(StretcherQuality::CONSISTENT)); +#endif + return module; } diff --git a/extern/audaspace/bindings/python/PySound.cpp b/extern/audaspace/bindings/python/PySound.cpp index a10f28e76cc..34b1f8d577a 100644 --- a/extern/audaspace/bindings/python/PySound.cpp +++ b/extern/audaspace/bindings/python/PySound.cpp @@ -64,6 +64,11 @@ #include "fx/ConvolverSound.h" #endif + +#ifdef WITH_RUBBERBAND +#include "fx/TimeStretchPitchScale.h" +#endif + #include #include #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION @@ -444,7 +449,7 @@ PyDoc_STRVAR(M_aud_Sound_sawtooth_doc, " :arg frequency: The frequency of the sawtooth wave in Hz.\n" " :type frequency: float\n" " :arg rate: The sampling rate in Hz. It's recommended to set this\n" - " value to the playback device's samling rate to avoid resamping.\n" + " value to the playback device's sampling rate to avoid resampling.\n" " :type rate: int\n" " :return: The created :class:`Sound` object.\n" " :rtype: :class:`Sound`"); @@ -482,7 +487,7 @@ PyDoc_STRVAR(M_aud_Sound_silence_doc, ".. classmethod:: silence(rate=48000)\n\n" " Creates a silence sound which plays simple silence.\n\n" " :arg rate: The sampling rate in Hz. It's recommended to set this\n" - " value to the playback device's samling rate to avoid resamping.\n" + " value to the playback device's sampling rate to avoid resampling.\n" " :type rate: int\n" " :return: The created :class:`Sound` object.\n" " :rtype: :class:`Sound`"); @@ -521,7 +526,7 @@ PyDoc_STRVAR(M_aud_Sound_sine_doc, " :arg frequency: The frequency of the sine wave in Hz.\n" " :type frequency: float\n" " :arg rate: The sampling rate in Hz. It's recommended to set this\n" - " value to the playback device's samling rate to avoid resamping.\n" + " value to the playback device's sampling rate to avoid resampling.\n" " :type rate: int\n" " :return: The created :class:`Sound` object.\n" " :rtype: :class:`Sound`"); @@ -561,7 +566,7 @@ PyDoc_STRVAR(M_aud_Sound_square_doc, " :arg frequency: The frequency of the square wave in Hz.\n" " :type frequency: float\n" " :arg rate: The sampling rate in Hz. It's recommended to set this\n" - " value to the playback device's samling rate to avoid resamping.\n" + " value to the playback device's sampling rate to avoid resampling.\n" " :type rate: int\n" " :return: The created :class:`Sound` object.\n" " :rtype: :class:`Sound`"); @@ -601,7 +606,7 @@ PyDoc_STRVAR(M_aud_Sound_triangle_doc, " :arg frequency: The frequency of the triangle wave in Hz.\n" " :type frequency: float\n" " :arg rate: The sampling rate in Hz. It's recommended to set this\n" - " value to the playback device's samling rate to avoid resamping.\n" + " value to the playback device's sampling rate to avoid resampling.\n" " :type rate: int\n" " :return: The created :class:`Sound` object.\n" " :rtype: :class:`Sound`"); @@ -1787,6 +1792,57 @@ Sound_binaural(Sound* self, PyObject* args) #endif +#ifdef WITH_RUBBERBAND + +PyDoc_STRVAR(M_aud_Sound_timeStretchPitchScale_doc, ".. method:: timeStretchPitchScale(time_ratio, 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 pitch_scale: The factor by which to adjust the pitch.\n" + " :type pitch_scale: float\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_timeStretchPitchScale(Sound* self, PyObject* args) +{ + double time_ratio, pitch_scale; + int quality = 0; + int preserve_formant = 0; + if(!PyArg_ParseTuple(args, "dd|i|p:timeStretchPitchScale", &time_ratio, &pitch_scale, &quality, &preserve_formant)) + return nullptr; + + if(quality < 0 || quality > 2) + { + PyErr_WarnEx(PyExc_UserWarning, "Invalid quality value: using default (0 = 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(new TimeStretchPitchScale(*reinterpret_cast*>(self->sound), time_ratio, pitch_scale, + static_cast(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 @@ -1900,6 +1956,11 @@ static PyMethodDef Sound_methods[] = { { "binaural", (PyCFunction)Sound_binaural, METH_VARARGS, M_aud_Sound_binaural_doc }, +#endif +#ifdef WITH_RUBBERBAND + {"timeStretchPitchScale", (PyCFunction)Sound_timeStretchPitchScale, METH_VARARGS, + M_aud_Sound_timeStretchPitchScale_doc + }, #endif {nullptr} /* Sentinel */ }; @@ -1923,7 +1984,7 @@ Sound_get_specs(Sound* self, void* nothing) } PyDoc_STRVAR(M_aud_Sound_length_doc, - "The length attribute returns the duration of the sound in seconds."); + "The sample specification of the sound as a tuple with rate and channel count."); static PyObject * Sound_get_length(Sound* self, void* nothing) diff --git a/extern/audaspace/blender_config.cmake b/extern/audaspace/blender_config.cmake index e3634ce6c37..981ca321bc8 100644 --- a/extern/audaspace/blender_config.cmake +++ b/extern/audaspace/blender_config.cmake @@ -14,6 +14,7 @@ if(DEFINED WITH_FFTW3 AND WITH_FFTW3) # "Build With FFTW" set(FFTW_LIBRARY ${FFTW3_LIBRARIES}) endif() set(WITH_LIBSNDFILE ${WITH_CODEC_SNDFILE}) # "Build With LibSndFile" +set(WITH_RUBBERBAND FALSE) # "Build With Rubber Band Library" set(SEPARATE_C FALSE) # "Build C Binding as separate library" set(PLUGIN_COREAUDIO FALSE) # "Build CoreAudio Plugin" set(PLUGIN_FFMPEG FALSE) # "Build FFMPEG Plugin" diff --git a/extern/audaspace/include/fx/TimeStretchPitchScale.h b/extern/audaspace/include/fx/TimeStretchPitchScale.h new file mode 100644 index 00000000000..397890791b7 --- /dev/null +++ b/extern/audaspace/include/fx/TimeStretchPitchScale.h @@ -0,0 +1,95 @@ +/******************************************************************************* + * 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 TimeStretchPitchScale.h + * @ingroup fx + * The TimeStretchPitchScale class. + */ + +#include "fx/Effect.h" + +AUD_NAMESPACE_BEGIN + +enum class StretcherQuality +{ + HIGH = 0, // Prioritize high-quality pitch processing + FAST = 1, // Prioritize speed over audio quality + CONSISTENT = 2 // Prioritize consistency for dynamic pitch changes +}; + +/** + * This sound allows a sound to be time-stretched and pitch scaled. + * \note The reader has to be seekable. + */ +class AUD_API TimeStretchPitchScale : public Effect +{ +private: + /** + * The factor by which to stretch or compress time. + */ + double m_timeRatio; + + /** + * The factor by which to adjust the pitch. + */ + double m_pitchScale; + + /** + * Rubberband stretcher quality. + */ + StretcherQuality m_quality; + + /** + * Whether to preserve the vocal formants during pitch-shifting + */ + bool m_preserveFormant; + + // delete copy constructor and operator= + TimeStretchPitchScale(const TimeStretchPitchScale&) = delete; + TimeStretchPitchScale& operator=(const TimeStretchPitchScale&) = delete; + +public: + /** + * Creates a new time-stretch, pitch scaled sound. + * \param sound The input sound. + * \param timeRatio The factor by which to stretch or compress time. + * \param pitchScale The factor by which to adjust the pitch. + * \param quality The processing quality level. + * \param preserveFormant Whether to preserve the vocal formants for the stretcher. + */ + TimeStretchPitchScale(std::shared_ptr sound, double timeRatio, double pitchScale, StretcherQuality quality, bool preserveFormant); + + /** + * Returns the time ratio. + */ + double getTimeRatio() const; + + /** + * Returns the pitch scale. + */ + double getPitchScale() const; + + /** + * Returns whether formant preservation is enabled. + */ + bool getPreserveFormant() const; + virtual std::shared_ptr createReader(); +}; + +AUD_NAMESPACE_END diff --git a/extern/audaspace/include/fx/TimeStretchPitchScaleReader.h b/extern/audaspace/include/fx/TimeStretchPitchScaleReader.h new file mode 100644 index 00000000000..909c1b7ddc4 --- /dev/null +++ b/extern/audaspace/include/fx/TimeStretchPitchScaleReader.h @@ -0,0 +1,125 @@ +/******************************************************************************* + * 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 TimeStretchPitchScaleReader.h + * @ingroup fx + * The TimeStretchPitchScaleReader class. + */ + +#include "TimeStretchPitchScale.h" + +#include "fx/EffectReader.h" +#include "rubberband/RubberBandStretcher.h" +#include "util/Buffer.h" + +using namespace RubberBand; + +AUD_NAMESPACE_BEGIN + +/** + * This class reads from another reader and applies time-stretching and pitch scaling. + */ +class AUD_API TimeStretchPitchScaleReader : public EffectReader +{ +private: + /** + * The current position. + */ + int m_position; + + /** + * Whether the reader has reached the end of stream. + */ + bool m_finishedReader; + + /** + * The input buffer for the reader. + */ + Buffer m_buffer; + + /** + * The input/output deinterleaved buffers for each channel. + */ + std::vector m_deinterleaved; + + /** + * The pointers to the input/output deinterleaved buffer data for processing/retrieving. + */ + std::vector m_channelData; + + /** + * Rubberband stretcher. + */ + std::unique_ptr m_stretcher; + + /** + * Number of samples that need to be dropped at the beginning or after a seek. + */ + int m_samplesToDrop; + + // delete copy constructor and operator= + TimeStretchPitchScaleReader(const TimeStretchPitchScaleReader&) = delete; + TimeStretchPitchScaleReader& operator=(const TimeStretchPitchScaleReader&) = delete; + + /** + * Feeds the number of required zeo samples to the stretcher and queries the amount of samples to drop. + */ + void reset(); + +public: + /** + * Creates a new stretcher reader. + * \param reader The reader to read from. + * \param timeRatio The factor by which to stretch or compress time. + * \param pitchScale The 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. + */ + TimeStretchPitchScaleReader(std::shared_ptr reader, double timeRatio, double pitchScale, StretcherQuality quality, bool preserveFormant); + + virtual void read(int& length, bool& eos, sample_t* buffer); + + virtual void seek(int position); + virtual int getLength() const; + virtual int getPosition() const; + + /** + * Retrieves the current time ratio for the stretcher. + * \return The current time ratio value. + */ + double getTimeRatio() const; + + /** + * Sets the time ratio for the stretcher. + */ + void setTimeRatio(double timeRatio); + + /** + * Retrieves the pitch scale for the stretcher. + * \return The current pitch scale value. + */ + double getPitchScale() const; + + /** + * Sets the pitch scale for the stretcher. + */ + void setPitchScale(double pitchScale); +}; + +AUD_NAMESPACE_END diff --git a/extern/audaspace/src/fx/TimeStretchPitchScale.cpp b/extern/audaspace/src/fx/TimeStretchPitchScale.cpp new file mode 100644 index 00000000000..a3c34a84fa5 --- /dev/null +++ b/extern/audaspace/src/fx/TimeStretchPitchScale.cpp @@ -0,0 +1,48 @@ +/******************************************************************************* + * 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/TimeStretchPitchScale.h" + +#include "fx/TimeStretchPitchScaleReader.h" + +AUD_NAMESPACE_BEGIN + +TimeStretchPitchScale::TimeStretchPitchScale(std::shared_ptr sound, double timeRatio, double pitchScale, StretcherQuality quality, bool preserveFormant) : + Effect(sound), m_timeRatio(timeRatio), m_pitchScale(pitchScale), m_quality(quality), m_preserveFormant(preserveFormant) + +{ +} + +std::shared_ptr TimeStretchPitchScale::createReader() +{ + return std::shared_ptr(new TimeStretchPitchScaleReader(getReader(), m_timeRatio, m_pitchScale, m_quality, m_preserveFormant)); +} + +double TimeStretchPitchScale::getTimeRatio() const +{ + return m_timeRatio; +} + +double TimeStretchPitchScale::getPitchScale() const +{ + return m_pitchScale; +} + +bool TimeStretchPitchScale::getPreserveFormant() const +{ + return m_preserveFormant; +} +AUD_NAMESPACE_END \ No newline at end of file diff --git a/extern/audaspace/src/fx/TimeStretchPitchScaleReader.cpp b/extern/audaspace/src/fx/TimeStretchPitchScaleReader.cpp new file mode 100644 index 00000000000..dbb55e60b2a --- /dev/null +++ b/extern/audaspace/src/fx/TimeStretchPitchScaleReader.cpp @@ -0,0 +1,208 @@ +/******************************************************************************* + * 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/TimeStretchPitchScaleReader.h" + +#include + +#include "Exception.h" +#include "IReader.h" + +#include "util/Buffer.h" + +using namespace RubberBand; + +AUD_NAMESPACE_BEGIN + +void TimeStretchPitchScaleReader::reset() +{ + auto startPad{m_stretcher->getPreferredStartPad()}; + + m_samplesToDrop = m_stretcher->getStartDelay(); + + m_deinterleaved[0].assureSize(startPad * sizeof(sample_t)); + std::memset(m_deinterleaved[0].getBuffer(), 0, startPad * sizeof(sample_t)); + + for(auto& channel : m_channelData) + channel = m_deinterleaved[0].getBuffer(); + + m_stretcher->process(m_channelData.data(), startPad, m_finishedReader); +} + +TimeStretchPitchScaleReader::TimeStretchPitchScaleReader(std::shared_ptr reader, double timeRatio, double pitchScale, StretcherQuality quality, bool preserveFormant) : + EffectReader(reader), m_position(0), m_finishedReader(false), m_channelData(reader->getSpecs().channels), m_deinterleaved(reader->getSpecs().channels) +{ + if (pitchScale < 1.0 / 256.0 || pitchScale > 256.0) + AUD_THROW(StateException, "The pitch scale must be between 1/256 and 256"); + + if (timeRatio < 1.0 / 256.0 || timeRatio > 256.0) + AUD_THROW(StateException, "The time-stretch ratio must be between 1/256 and 256"); + + RubberBandStretcher::Options options = RubberBandStretcher::OptionProcessRealTime | RubberBandStretcher::OptionEngineFiner | RubberBandStretcher::OptionChannelsTogether; + + switch(quality) + { + case StretcherQuality::HIGH: + options |= RubberBandStretcher::OptionPitchHighQuality; + break; + case StretcherQuality::FAST: + options |= RubberBandStretcher::OptionPitchHighSpeed; + options |= RubberBandStretcher::OptionWindowShort; + break; + case StretcherQuality::CONSISTENT: + options |= RubberBandStretcher::OptionPitchHighConsistency; + break; + default: + break; + } + + options |= preserveFormant ? RubberBandStretcher::OptionFormantPreserved : RubberBandStretcher::OptionFormantShifted; + m_stretcher = std::make_unique(m_reader->getSpecs().rate, m_reader->getSpecs().channels, options, timeRatio, pitchScale); + + reset(); +} + +void TimeStretchPitchScaleReader::read(int& length, bool& eos, sample_t* buffer) +{ + if(length == 0) + return; + + int samplesize = AUD_SAMPLE_SIZE(m_reader->getSpecs()); + int channels = m_reader->getSpecs().channels; + + sample_t* buf; + + int samplesRead = 0; + + eos = false; + while(samplesRead < length) + { + int len = m_stretcher->getSamplesRequired(); + if(!m_finishedReader && len != 0) + { + m_buffer.assureSize(len * samplesize); + sample_t* buf = m_buffer.getBuffer(); + + m_reader->read(len, m_finishedReader, buf); + + // Deinterleave the input reader buffer for processing + for(int channel = 0; channel < channels; channel++) + { + m_deinterleaved[channel].assureSize(len * sizeof(sample_t)); + sample_t* channelBuf = m_deinterleaved[channel].getBuffer(); + + for(int i = 0; i < len; i++) + { + channelBuf[i] = buf[i * channels + channel]; + } + + m_channelData[channel] = channelBuf; + } + + m_stretcher->process(m_channelData.data(), len, m_finishedReader); + } + + int available = m_stretcher->available(); + if(available == -1) + { + eos = true; + break; + } + + if(available == 0) + continue; + + available = std::min(m_samplesToDrop ? m_samplesToDrop : length - samplesRead, available); + + for(int channel = 0; channel < channels; channel++) + { + m_deinterleaved[channel].assureSize(available * sizeof(sample_t)); + m_channelData[channel] = m_deinterleaved[channel].getBuffer(); + } + + m_stretcher->retrieve(m_channelData.data(), available); + + if(m_samplesToDrop) + { + m_samplesToDrop -= available; + } + else + { + // Interleave the retrieved data into the buffer + for(int channel = 0; channel < channels; channel++) + { + sample_t* outputBuf = m_deinterleaved[channel].getBuffer(); + for(int i = 0; i < available; i++) + { + buffer[(samplesRead + i) * channels + channel] = outputBuf[i]; + } + } + + samplesRead += available; + } + } + + length = samplesRead; + m_position += length; + eos = m_stretcher->available() == -1; +} + +double TimeStretchPitchScaleReader::getTimeRatio() const +{ + return m_stretcher->getTimeRatio(); +} + +void TimeStretchPitchScaleReader::setTimeRatio(double timeRatio) +{ + if(timeRatio >= 1.0 / 256.0 && timeRatio <= 256.0) + { + m_stretcher->setTimeRatio(timeRatio); + } +} + +double TimeStretchPitchScaleReader::getPitchScale() const +{ + return m_stretcher->getPitchScale(); +} + +void TimeStretchPitchScaleReader::setPitchScale(double pitchScale) +{ + if(pitchScale >= 1.0 / 256.0 && pitchScale <= 256.0) + { + m_stretcher->setPitchScale(pitchScale); + } +} + +void TimeStretchPitchScaleReader::seek(int position) +{ + m_reader->seek(int(position / getTimeRatio())); + m_finishedReader = false; + m_stretcher->reset(); + reset(); + m_position = position; +} + +int TimeStretchPitchScaleReader::getLength() const +{ + return m_reader->getLength() * getTimeRatio(); +} + +int TimeStretchPitchScaleReader::getPosition() const +{ + return m_position; +} + +AUD_NAMESPACE_END