From 804849653571b8ff6e65789a78a144bec67acfef Mon Sep 17 00:00:00 2001 From: lisyarus Date: Sat, 8 Oct 2022 19:07:30 +0300 Subject: [PATCH] Implement pitch control audio effect --- .../include/psemek/audio/effect/pitch.hpp | 20 ++++ libs/audio/include/psemek/audio/resampler.hpp | 15 ++- libs/audio/source/effect/pitch.cpp | 104 ++++++++++++++++++ libs/audio/source/resampler.cpp | 40 ++++++- libs/audio/source/track_mp3.cpp | 3 +- 5 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 libs/audio/include/psemek/audio/effect/pitch.hpp create mode 100644 libs/audio/source/effect/pitch.cpp diff --git a/libs/audio/include/psemek/audio/effect/pitch.hpp b/libs/audio/include/psemek/audio/effect/pitch.hpp new file mode 100644 index 00000000..b39e1ca0 --- /dev/null +++ b/libs/audio/include/psemek/audio/effect/pitch.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace psemek::audio +{ + + struct pitch_control + : stream + { + virtual float pitch() const = 0; + virtual float pitch(float ratio) = 0; + + virtual float smoothness() const = 0; + virtual float smoothness(float value) = 0; + }; + + std::shared_ptr pitch(stream_ptr stream, float ratio = 1.f, float smoothness = 0.01f); + +} diff --git a/libs/audio/include/psemek/audio/resampler.hpp b/libs/audio/include/psemek/audio/resampler.hpp index 4985f561..b5eaa124 100644 --- a/libs/audio/include/psemek/audio/resampler.hpp +++ b/libs/audio/include/psemek/audio/resampler.hpp @@ -3,14 +3,23 @@ #include #include +#include namespace psemek::audio { struct resampler { + resampler(float ratio = 1.f, float smoothness = 0.f); + + float ratio() const; + float ratio(float value); + + float smoothness() const; + float smoothness(float value); + // ratio is target frequency / source frequency - void feed(util::span samples, float ratio); + void feed(util::span samples); util::span result() const { @@ -24,6 +33,10 @@ namespace psemek::audio float last_sample_[2] {0.f, 0.f}; + std::atomic ratio_{1.f}; + std::atomic smoothness_multiplier_{1.f}; + float real_ratio_{1.f}; + void advance(float delta); }; diff --git a/libs/audio/source/effect/pitch.cpp b/libs/audio/source/effect/pitch.cpp new file mode 100644 index 00000000..c8d7fde1 --- /dev/null +++ b/libs/audio/source/effect/pitch.cpp @@ -0,0 +1,104 @@ +#include +#include + +#include + +namespace psemek::audio +{ + + namespace + { + + struct pitch_control_impl + : pitch_control + { + pitch_control_impl(stream_ptr stream, float ratio, float smoothness) + : stream_(std::move(stream)) + , resampler_(1.f / ratio, smoothness) + {} + + // N.B. resampler ratio is in terms of sampling frequency, while + // pitch control ratio is in terms of sound frequency, which + // is the opposite + + float pitch() const override + { + return 1.f / resampler_.ratio(); + } + + float pitch(float ratio) override + { + return 1.f / resampler_.ratio(1.f / ratio); + } + + float smoothness() const override + { + return resampler_.smoothness(); + } + + float smoothness(float value) override + { + return resampler_.smoothness(value); + } + + std::optional length() const override + { + return std::nullopt; + } + + std::size_t read(float * data, std::size_t sample_count) override + { + std::size_t result = 0; + + while (result < sample_count) + { + if (resampler_pos_ < resampler_.result().size()) + { + std::size_t size = std::min(sample_count - result, resampler_.result().size() - resampler_pos_); + std::copy(resampler_.result().data() + resampler_pos_, resampler_.result().data() + resampler_pos_ + size, data + result); + result += size; + resampler_pos_ += size; + played_ += size; + } + else + { + resampler_pos_ = 0; + + std::size_t request_size = std::max(sample_count, std::ceil(resampler_.ratio() * sample_count / 2.f) * 2); + source_buffer_.resize(request_size); + auto count = stream_->read(source_buffer_.data(), request_size); + + if (count == 0) + break; + + resampler_.feed({source_buffer_.data(), source_buffer_.data() + count}); + } + } + + return result; + } + + std::size_t played() const override + { + return played_.load(); + } + + private: + stream_ptr stream_; + + std::vector source_buffer_; + + resampler resampler_; + std::size_t resampler_pos_{0}; + + std::atomic played_{0}; + }; + + } + + std::shared_ptr pitch(stream_ptr stream, float ratio, float smoothness) + { + return std::make_shared(std::move(stream), ratio, smoothness); + } + +} diff --git a/libs/audio/source/resampler.cpp b/libs/audio/source/resampler.cpp index 9b959619..1bd943d7 100644 --- a/libs/audio/source/resampler.cpp +++ b/libs/audio/source/resampler.cpp @@ -1,32 +1,64 @@ #include +#include #include +#include + #include namespace psemek::audio { - void resampler::feed(util::span samples, float ratio) + resampler::resampler(float ratio, float smoothness) + : ratio_(ratio) + , smoothness_multiplier_(smoothness_to_multiplier(smoothness)) + , real_ratio_(ratio) + {} + + float resampler::ratio() const + { + return ratio_.load(); + } + + float resampler::ratio(float value) + { + return ratio_.exchange(value); + } + + float resampler::smoothness() const + { + return multiplier_to_smoothness(smoothness_multiplier_.load()); + } + + float resampler::smoothness(float value) + { + return multiplier_to_smoothness(smoothness_multiplier_.exchange(smoothness_to_multiplier(value))); + } + + void resampler::feed(util::span samples) { result_.clear(); if (samples.empty()) return; - float const delta = 1.f / ratio; + float const ratio = ratio_.load(); + float const smoothness_multiplier = smoothness_multiplier_.load(); while (position_ < 0) { result_.push_back(geom::lerp(last_sample_[0], samples[0], position_frac_)); result_.push_back(geom::lerp(last_sample_[1], samples[1], position_frac_)); - advance(delta); + real_ratio_ += (ratio - real_ratio_) * smoothness_multiplier; + advance(1.f / real_ratio_); } while (2 * position_ + 3 < samples.size()) { result_.push_back(geom::lerp(samples[2 * position_ + 0], samples[2 * position_ + 2], position_frac_)); result_.push_back(geom::lerp(samples[2 * position_ + 1], samples[2 * position_ + 3], position_frac_)); - advance(delta); + real_ratio_ += (ratio - real_ratio_) * smoothness_multiplier; + advance(1.f / real_ratio_); } position_ -= static_cast(samples.size()) / 2; diff --git a/libs/audio/source/track_mp3.cpp b/libs/audio/source/track_mp3.cpp index cdd96ebf..b690379e 100644 --- a/libs/audio/source/track_mp3.cpp +++ b/libs/audio/source/track_mp3.cpp @@ -78,7 +78,8 @@ namespace psemek::audio source_buffer_size *= 2; } - resampler_.feed({source_buffer_.data(), source_buffer_.data() + source_buffer_size}, static_cast(frequency) / frame_info.hz); + resampler_.ratio(static_cast(frequency) / frame_info.hz); + resampler_.feed({source_buffer_.data(), source_buffer_.data() + source_buffer_size}); } }