Add audio envelope helper based on a sequence of linear or exponential segments
All checks were successful
Run tests / Run tests (push) Successful in 6m46s

This commit is contained in:
Nikita Lisitsa 2026-07-18 22:32:47 +03:00
parent 748035c514
commit 4de0499d29
2 changed files with 157 additions and 0 deletions

View file

@ -0,0 +1,72 @@
#include <psemek/audio/stream.hpp>
#include <psemek/audio/duration.hpp>
#include <vector>
namespace psemek::audio
{
struct segment
{
enum segment_type
{
linear,
exponential,
};
segment_type type;
audio::duration duration;
};
constexpr struct keep_playing_tag {} keep_playing;
// values.size() must equal smoothing.size() + 1
// truncates the stream in the end if keep_playing isn't specified
stream_ptr envelope(stream_ptr stream, std::vector<float> values, std::vector<segment> segments);
stream_ptr envelope(stream_ptr stream, keep_playing_tag, std::vector<float> values, std::vector<segment> segments);
namespace detail
{
inline void envelope_helper(std::vector<float> &, std::vector<segment> &)
{}
template <typename ... Args>
void envelope_helper(std::vector<float> & values, std::vector<segment> & segments, segment::segment_type type, duration duration, float value, Args ... args)
{
values.push_back(value);
segments.push_back({type, duration});
envelope_helper(values, segments, args...);
}
template <typename ... Args>
stream_ptr envelope_impl(stream_ptr stream, bool keep_playing, float first_value, Args ... args)
{
std::vector<float> values;
std::vector<segment> segments;
values.push_back(first_value);
envelope_helper(values, segments, args...);
if (keep_playing)
return audio::envelope(std::move(stream), audio::keep_playing, std::move(values), std::move(segments));
return audio::envelope(std::move(stream), std::move(values), std::move(segments));
}
}
// Must be called alternating volume levels, segment types, and segment durations,
// starting and ending with a volume level, e.g.
// envelope(stream, 0.4, segment::linear, 100_ms, 1.2, segment::exponential, 400_ms, 5.3)
template <typename ... Args>
stream_ptr envelope(stream_ptr stream, float first_value, Args ... args)
{
return detail::envelope_impl(std::move(stream), false, first_value, args...);
}
template <typename ... Args>
stream_ptr envelope(stream_ptr stream, keep_playing_tag, float first_value, Args ... args)
{
return detail::envelope_impl(std::move(stream), true, first_value, args...);
}
}

View file

@ -0,0 +1,85 @@
#include <psemek/audio/effect/envelope.hpp>
#include <psemek/audio/effect/envelope_base.hpp>
#include <psemek/util/exception.hpp>
#include <psemek/util/enum.hpp>
namespace psemek::audio
{
namespace
{
float compute_param(float v0, float v1, segment::segment_type type)
{
switch (type)
{
case segment::linear:
return v1 - v0;
case segment::exponential:
// f(t) = Aexp(Bt)
// t=0 => A=v0
// t=1 => Aexp(B)=v1 => B = log(v1/v0)
return std::log(v1 / v0);
}
throw util::unknown_enum_value_exception(type);
}
float interpolate(float v0, float /* v1 */, float t, segment::segment_type type, float param)
{
switch (type)
{
case segment::linear:
return v0 + param * t;
case segment::exponential:
return v0 * std::exp(param * t);
}
throw util::unknown_enum_value_exception(type);
}
stream_ptr envelope_impl(stream_ptr stream, bool keep_playing, std::vector<float> values, std::vector<segment> segments)
{
if (values.size() != segments.size() + 1)
throw util::exception("Invalid envelope");
std::optional<duration> truncate;
if (!keep_playing)
{
truncate = duration::from_frames(0);
for (auto const & segment : segments)
*truncate += segment.duration;
}
std::vector<float> params(segments.size());
for (std::size_t i = 0; i < segments.size(); ++i)
params[i] = compute_param(values[i], values[i + 1], segments[i].type);
return envelope_base(std::move(stream), [values = std::move(values), segments = std::move(segments), params = std::move(params), index = 0, played = std::size_t{0}](duration) mutable {
played += 1;
while (index < segments.size() && played >= segments[index].duration.frames())
{
++index;
played = 0;
}
if (index == segments.size())
return values.back();
return interpolate(values[index], values[index + 1], played * 1.f / segments[index].duration.frames(), segments[index].type, params[index]);
}, truncate);
}
}
stream_ptr envelope(stream_ptr stream, std::vector<float> values, std::vector<segment> segments)
{
return envelope_impl(std::move(stream), false, std::move(values), std::move(segments));
}
stream_ptr envelope(stream_ptr stream, keep_playing_tag, std::vector<float> values, std::vector<segment> segments)
{
return envelope_impl(std::move(stream), true, std::move(values), std::move(segments));
}
}