Compare commits
6 commits
3713f620bb
...
4de0499d29
| Author | SHA1 | Date | |
|---|---|---|---|
| 4de0499d29 | |||
| 748035c514 | |||
| 20fbb9c5ac | |||
| 2d66f12707 | |||
| c177911737 | |||
| dbf5117dc6 |
8 changed files with 277 additions and 134 deletions
|
|
@ -3,7 +3,6 @@
|
||||||
#include <psemek/audio/constants.hpp>
|
#include <psemek/audio/constants.hpp>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace psemek::audio
|
namespace psemek::audio
|
||||||
{
|
{
|
||||||
|
|
|
||||||
72
libs/audio/include/psemek/audio/effect/envelope.hpp
Normal file
72
libs/audio/include/psemek/audio/effect/envelope.hpp
Normal 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...);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
69
libs/audio/include/psemek/audio/effect/envelope_base.hpp
Normal file
69
libs/audio/include/psemek/audio/effect/envelope_base.hpp
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#include <psemek/audio/stream.hpp>
|
||||||
|
#include <psemek/audio/duration.hpp>
|
||||||
|
|
||||||
|
namespace psemek::audio
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
template <typename EnvelopeFn>
|
||||||
|
struct envelope_base_impl
|
||||||
|
: stream
|
||||||
|
{
|
||||||
|
envelope_base_impl(stream_ptr stream, EnvelopeFn envelope_fn, std::optional<duration> truncate)
|
||||||
|
: stream_(std::move(stream))
|
||||||
|
, envelope_fn_(std::move(envelope_fn))
|
||||||
|
, remaining_(truncate ? std::optional{truncate->samples()} : std::nullopt)
|
||||||
|
, total_length_(truncate ? stream_->played() + *remaining_ : stream_->length())
|
||||||
|
{}
|
||||||
|
|
||||||
|
std::optional<std::size_t> length() const override
|
||||||
|
{
|
||||||
|
return total_length_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t read(util::span<float> samples) override
|
||||||
|
{
|
||||||
|
std::size_t const max_count = remaining_ ? std::min(*remaining_, samples.size()) : samples.size();
|
||||||
|
|
||||||
|
auto const count = stream_->read(samples.prefix(max_count));
|
||||||
|
if (remaining_)
|
||||||
|
*remaining_ -= count;
|
||||||
|
|
||||||
|
auto out = samples.data();
|
||||||
|
for (std::size_t i = 0; i < count; i += 2)
|
||||||
|
{
|
||||||
|
float gain = envelope_fn_(played_);
|
||||||
|
out[0] *= gain;
|
||||||
|
out[1] *= gain;
|
||||||
|
out += 2;
|
||||||
|
played_ += duration::from_frames(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t played() const override
|
||||||
|
{
|
||||||
|
return stream_->played();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
stream_ptr stream_;
|
||||||
|
EnvelopeFn envelope_fn_;
|
||||||
|
duration played_;
|
||||||
|
std::optional<std::size_t> remaining_;
|
||||||
|
std::optional<std::size_t> total_length_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteed to call envelope_fn exactly once per audio frame
|
||||||
|
template <typename EnvelopeFn>
|
||||||
|
stream_ptr envelope_base(stream_ptr stream, EnvelopeFn envelope_fn, std::optional<duration> truncate = {})
|
||||||
|
{
|
||||||
|
return std::make_shared<envelope_base_impl<EnvelopeFn>>(std::move(stream), std::move(envelope_fn), truncate);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
libs/audio/include/psemek/audio/literals.hpp
Normal file
31
libs/audio/include/psemek/audio/literals.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <psemek/audio/duration.hpp>
|
||||||
|
#include <psemek/audio/constants.hpp>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace psemek::audio::literals
|
||||||
|
{
|
||||||
|
|
||||||
|
duration operator ""_s (unsigned long long value)
|
||||||
|
{
|
||||||
|
return duration::from_frames(value * frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
duration operator ""_ms (unsigned long long value)
|
||||||
|
{
|
||||||
|
return duration::from_frames((value * frequency) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
duration operator ""_s (long double value)
|
||||||
|
{
|
||||||
|
return duration::from_frames(std::round(value * frequency));
|
||||||
|
}
|
||||||
|
|
||||||
|
duration operator ""_ms (long double value)
|
||||||
|
{
|
||||||
|
return duration::from_frames(std::round(value * frequency / 1000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
85
libs/audio/source/effect/envelope.cpp
Normal file
85
libs/audio/source/effect/envelope.cpp
Normal 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,71 +1,15 @@
|
||||||
#include <psemek/audio/effect/fade_in.hpp>
|
#include <psemek/audio/effect/fade_out.hpp>
|
||||||
#include <psemek/audio/constants.hpp>
|
#include <psemek/audio/effect/envelope_base.hpp>
|
||||||
|
#include <psemek/math/interval.hpp>
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace psemek::audio
|
namespace psemek::audio
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
struct fade_in_impl
|
|
||||||
: stream
|
|
||||||
{
|
|
||||||
fade_in_impl(stream_ptr stream, duration length, duration start)
|
|
||||||
: stream_(std::move(stream))
|
|
||||||
, length_(length)
|
|
||||||
, start_(start)
|
|
||||||
{}
|
|
||||||
|
|
||||||
std::optional<std::size_t> length() const override
|
|
||||||
{
|
|
||||||
return stream_->length();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t played() const override
|
|
||||||
{
|
|
||||||
return stream_->played();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t read(util::span<float> samples) override
|
|
||||||
{
|
|
||||||
auto const count = stream_->read(samples);
|
|
||||||
std::fill(samples.begin(), samples.begin() + std::min<std::size_t>(count, start_.samples()), 0.f);
|
|
||||||
|
|
||||||
if (count <= start_.samples())
|
|
||||||
{
|
|
||||||
start_ -= count;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (std::size_t i = start_.samples(); i < count; i += 2)
|
|
||||||
{
|
|
||||||
float m = static_cast<float>(std::min<std::size_t>(current_, length_.samples())) / length_.samples();
|
|
||||||
|
|
||||||
samples[i + 0] *= m;
|
|
||||||
samples[i + 1] *= m;
|
|
||||||
|
|
||||||
current_ += 2;
|
|
||||||
}
|
|
||||||
start_ = duration{};
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
stream_ptr stream_;
|
|
||||||
duration length_;
|
|
||||||
duration start_;
|
|
||||||
std::size_t current_ = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
stream_ptr fade_in(stream_ptr stream, duration length, duration start)
|
stream_ptr fade_in(stream_ptr stream, duration length, duration start)
|
||||||
{
|
{
|
||||||
return std::make_shared<fade_in_impl>(std::move(stream), length, start);
|
return envelope_base(std::move(stream), [start, length](duration time){
|
||||||
|
return math::clamp(static_cast<float>(time.frames() - start.frames()) / length.frames(), {0.f, 1.f});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,15 @@
|
||||||
#include <psemek/audio/effect/fade_out.hpp>
|
#include <psemek/audio/effect/fade_out.hpp>
|
||||||
#include <psemek/audio/constants.hpp>
|
#include <psemek/audio/effect/envelope_base.hpp>
|
||||||
|
#include <psemek/math/interval.hpp>
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
namespace psemek::audio
|
namespace psemek::audio
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
struct fade_out_impl
|
|
||||||
: stream
|
|
||||||
{
|
|
||||||
fade_out_impl(stream_ptr stream, duration length, duration start)
|
|
||||||
: stream_(std::move(stream))
|
|
||||||
, length_(length)
|
|
||||||
, start_(start)
|
|
||||||
, total_length_(stream_->played() + start_.samples() + length_.samples())
|
|
||||||
{}
|
|
||||||
|
|
||||||
std::optional<std::size_t> length() const override
|
|
||||||
{
|
|
||||||
return total_length_;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t played() const override
|
|
||||||
{
|
|
||||||
return stream_->played();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t read(util::span<float> samples) override
|
|
||||||
{
|
|
||||||
if (current_ >= length_.samples())
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
auto result = stream_->read(samples);
|
|
||||||
|
|
||||||
if (result <= start_.samples())
|
|
||||||
{
|
|
||||||
start_ -= duration::from_samples(result);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (std::size_t i = start_.samples(); i < result; i += 2)
|
|
||||||
{
|
|
||||||
float m = static_cast<float>(length_.samples() - std::min<std::size_t>(current_, length_.samples())) / length_.samples();
|
|
||||||
|
|
||||||
samples[i + 0] *= m;
|
|
||||||
samples[i + 1] *= m;
|
|
||||||
|
|
||||||
current_ += 2;
|
|
||||||
}
|
|
||||||
start_ = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
stream_ptr stream_;
|
|
||||||
duration length_;
|
|
||||||
duration start_;
|
|
||||||
std::size_t current_ = 0;
|
|
||||||
std::size_t total_length_ = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
stream_ptr fade_out(stream_ptr stream, duration length, duration start)
|
stream_ptr fade_out(stream_ptr stream, duration length, duration start)
|
||||||
{
|
{
|
||||||
return std::make_shared<fade_out_impl>(std::move(stream), length, start);
|
return envelope_base(std::move(stream), [start, length](duration time){
|
||||||
|
return math::clamp(static_cast<float>(length.frames() + start.frames() - time.frames()) / length.frames(), {0.f, 1.f});
|
||||||
|
}, start + length);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,21 @@ namespace psemek::audio
|
||||||
{
|
{
|
||||||
truncate_impl(stream_ptr stream, duration length)
|
truncate_impl(stream_ptr stream, duration length)
|
||||||
: stream_(std::move(stream))
|
: stream_(std::move(stream))
|
||||||
, length_(length)
|
, remaining_(length.samples())
|
||||||
|
, total_length_(stream_->played() + remaining_)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
std::optional<std::size_t> length() const override
|
std::optional<std::size_t> length() const override
|
||||||
{
|
{
|
||||||
return length_.samples();
|
return total_length_;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t read(util::span<float> samples) override
|
std::size_t read(util::span<float> samples) override
|
||||||
{
|
{
|
||||||
auto played = stream_->played();
|
auto const max_count = std::min<std::size_t>(remaining_, samples.size());
|
||||||
auto max_count = std::min<std::size_t>(length_.samples() - played, samples.size());
|
auto const count = stream_->read(samples.prefix(max_count));
|
||||||
return stream_->read(samples.prefix(max_count));
|
remaining_ -= count;
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t played() const override
|
std::size_t played() const override
|
||||||
|
|
@ -35,7 +37,8 @@ namespace psemek::audio
|
||||||
|
|
||||||
private:
|
private:
|
||||||
stream_ptr stream_;
|
stream_ptr stream_;
|
||||||
duration length_;
|
std::size_t remaining_;
|
||||||
|
std::size_t total_length_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue