Implement pitch control audio effect
This commit is contained in:
parent
e74f5957c0
commit
8048496535
5 changed files with 176 additions and 6 deletions
20
libs/audio/include/psemek/audio/effect/pitch.hpp
Normal file
20
libs/audio/include/psemek/audio/effect/pitch.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <psemek/audio/stream.hpp>
|
||||||
|
|
||||||
|
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_control> pitch(stream_ptr stream, float ratio = 1.f, float smoothness = 0.01f);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,14 +3,23 @@
|
||||||
#include <psemek/util/span.hpp>
|
#include <psemek/util/span.hpp>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
namespace psemek::audio
|
namespace psemek::audio
|
||||||
{
|
{
|
||||||
|
|
||||||
struct resampler
|
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
|
// ratio is target frequency / source frequency
|
||||||
void feed(util::span<float const> samples, float ratio);
|
void feed(util::span<float const> samples);
|
||||||
|
|
||||||
util::span<float const> result() const
|
util::span<float const> result() const
|
||||||
{
|
{
|
||||||
|
|
@ -24,6 +33,10 @@ namespace psemek::audio
|
||||||
|
|
||||||
float last_sample_[2] {0.f, 0.f};
|
float last_sample_[2] {0.f, 0.f};
|
||||||
|
|
||||||
|
std::atomic<float> ratio_{1.f};
|
||||||
|
std::atomic<float> smoothness_multiplier_{1.f};
|
||||||
|
float real_ratio_{1.f};
|
||||||
|
|
||||||
void advance(float delta);
|
void advance(float delta);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
104
libs/audio/source/effect/pitch.cpp
Normal file
104
libs/audio/source/effect/pitch.cpp
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
#include <psemek/audio/effect/pitch.hpp>
|
||||||
|
#include <psemek/audio/resampler.hpp>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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<std::size_t> 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<std::size_t>(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<float> source_buffer_;
|
||||||
|
|
||||||
|
resampler resampler_;
|
||||||
|
std::size_t resampler_pos_{0};
|
||||||
|
|
||||||
|
std::atomic<std::size_t> played_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<pitch_control> pitch(stream_ptr stream, float ratio, float smoothness)
|
||||||
|
{
|
||||||
|
return std::make_shared<pitch_control_impl>(std::move(stream), ratio, smoothness);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,64 @@
|
||||||
#include <psemek/audio/resampler.hpp>
|
#include <psemek/audio/resampler.hpp>
|
||||||
|
#include <psemek/audio/smooth.hpp>
|
||||||
#include <psemek/geom/math.hpp>
|
#include <psemek/geom/math.hpp>
|
||||||
|
|
||||||
|
#include <psemek/log/log.hpp>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace psemek::audio
|
namespace psemek::audio
|
||||||
{
|
{
|
||||||
|
|
||||||
void resampler::feed(util::span<float const> 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<float const> samples)
|
||||||
{
|
{
|
||||||
result_.clear();
|
result_.clear();
|
||||||
|
|
||||||
if (samples.empty())
|
if (samples.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
float const delta = 1.f / ratio;
|
float const ratio = ratio_.load();
|
||||||
|
float const smoothness_multiplier = smoothness_multiplier_.load();
|
||||||
|
|
||||||
while (position_ < 0)
|
while (position_ < 0)
|
||||||
{
|
{
|
||||||
result_.push_back(geom::lerp(last_sample_[0], samples[0], position_frac_));
|
result_.push_back(geom::lerp(last_sample_[0], samples[0], position_frac_));
|
||||||
result_.push_back(geom::lerp(last_sample_[1], samples[1], 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())
|
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_ + 0], samples[2 * position_ + 2], position_frac_));
|
||||||
result_.push_back(geom::lerp(samples[2 * position_ + 1], samples[2 * position_ + 3], 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<int>(samples.size()) / 2;
|
position_ -= static_cast<int>(samples.size()) / 2;
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,8 @@ namespace psemek::audio
|
||||||
source_buffer_size *= 2;
|
source_buffer_size *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
resampler_.feed({source_buffer_.data(), source_buffer_.data() + source_buffer_size}, static_cast<float>(frequency) / frame_info.hz);
|
resampler_.ratio(static_cast<float>(frequency) / frame_info.hz);
|
||||||
|
resampler_.feed({source_buffer_.data(), source_buffer_.data() + source_buffer_size});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue