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