From 4de0499d2900b035f66936ed74574b27fbe4b240 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Sat, 18 Jul 2026 22:32:47 +0300 Subject: [PATCH] Add audio envelope helper based on a sequence of linear or exponential segments --- .../include/psemek/audio/effect/envelope.hpp | 72 ++++++++++++++++ libs/audio/source/effect/envelope.cpp | 85 +++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 libs/audio/include/psemek/audio/effect/envelope.hpp create mode 100644 libs/audio/source/effect/envelope.cpp diff --git a/libs/audio/include/psemek/audio/effect/envelope.hpp b/libs/audio/include/psemek/audio/effect/envelope.hpp new file mode 100644 index 00000000..4056f082 --- /dev/null +++ b/libs/audio/include/psemek/audio/effect/envelope.hpp @@ -0,0 +1,72 @@ +#include +#include + +#include + +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 values, std::vector segments); + stream_ptr envelope(stream_ptr stream, keep_playing_tag, std::vector values, std::vector segments); + + namespace detail + { + + inline void envelope_helper(std::vector &, std::vector &) + {} + + template + void envelope_helper(std::vector & values, std::vector & 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 + stream_ptr envelope_impl(stream_ptr stream, bool keep_playing, float first_value, Args ... args) + { + std::vector values; + std::vector 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 + stream_ptr envelope(stream_ptr stream, float first_value, Args ... args) + { + return detail::envelope_impl(std::move(stream), false, first_value, args...); + } + + template + 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...); + } + +} diff --git a/libs/audio/source/effect/envelope.cpp b/libs/audio/source/effect/envelope.cpp new file mode 100644 index 00000000..ed724e92 --- /dev/null +++ b/libs/audio/source/effect/envelope.cpp @@ -0,0 +1,85 @@ +#include +#include +#include +#include + +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 values, std::vector segments) + { + if (values.size() != segments.size() + 1) + throw util::exception("Invalid envelope"); + + std::optional truncate; + if (!keep_playing) + { + truncate = duration::from_frames(0); + for (auto const & segment : segments) + *truncate += segment.duration; + } + + std::vector 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 values, std::vector 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 values, std::vector segments) + { + return envelope_impl(std::move(stream), true, std::move(values), std::move(segments)); + } + +}