Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
reverse.hpp
Go to the documentation of this file.
1#pragma once
2
3// Reverse segmenter — double-buffered capture/playback, one per channel.
4//
5// The standard reverse-delay construction: one buffer is written forward while
6// the other is read backward, and the two swap when the write head reaches the
7// end of a segment. Segment length is the current SLEWED delay time, so the
8// reversed playback lines up with what the delay time claims and a time change
9// still glides rather than jumping.
10//
11// Two details make it usable rather than merely correct:
12//
13// * A raised-cosine window over the first and last kReverseFadeSamples of
14// each segment. Reversal splices the newest sample of one segment against
15// the oldest of the next; without the fade that discontinuity is an
16// audible tick on every segment boundary, and inside a feedback loop it is
17// a tick that recirculates.
18// * A FRACTIONAL read position. Tape's wow and flutter modulate the read
19// head, and they have to keep doing that when the transport is reversed —
20// otherwise engaging reverse silently switches the tape character's
21// defining instability off.
22
25
26#include <algorithm>
27#include <cmath>
28#include <cstddef>
29#include <vector>
30
32
34public:
42 static constexpr std::size_t minimum_capacity_samples() noexcept {
43 return static_cast<std::size_t>(2 * kReverseFadeSamples);
44 }
45
51 void prepare(std::size_t capacity_samples) {
52 capacity_ = std::max(capacity_samples, minimum_capacity_samples());
53 capture_.assign(capacity_, 0.0f);
54 playback_.assign(capacity_, 0.0f);
55 reset();
56 }
57
58 void reset() noexcept {
59 std::fill(capture_.begin(), capture_.end(), 0.0f);
60 std::fill(playback_.begin(), playback_.end(), 0.0f);
61 position_ = 0;
62 capture_length_ = minimum_capacity_samples();
63 playback_length_ = minimum_capacity_samples();
64 }
65
70 double process(double x, double segment_samples, double mod_offset_samples) noexcept {
71 const auto requested = static_cast<std::size_t>(
72 std::clamp(segment_samples, static_cast<double>(minimum_capacity_samples()),
73 static_cast<double>(capacity_)));
74
75 capture_[position_] = static_cast<float>(x);
76
77 const double backward =
78 static_cast<double>(playback_length_ - 1u) - static_cast<double>(position_);
79 const double read_position = std::clamp(
80 backward + mod_offset_samples, 0.0, static_cast<double>(playback_length_ - 1u));
81 const double sample = interpolate(playback_, playback_length_, read_position);
82 const double windowed = sample * boundary_gain();
83
84 if (++position_ >= capture_length_) {
85 capture_.swap(playback_);
86 playback_length_ = capture_length_;
87 std::fill(capture_.begin(), capture_.end(), 0.0f);
88 capture_length_ = requested;
89 position_ = 0;
90 }
91 return windowed;
92 }
93
94private:
96 double boundary_gain() const noexcept {
97 const auto fade = static_cast<std::size_t>(kReverseFadeSamples);
98 const std::size_t from_end = capture_length_ - 1u - position_;
99 const std::size_t nearest = std::min(position_, from_end);
100 if (nearest >= fade) return 1.0;
101 const double t = static_cast<double>(nearest) / static_cast<double>(fade);
102 return 0.5 * (1.0 - std::cos(kPi * t));
103 }
104
108 static double interpolate(const std::vector<float>& buffer, std::size_t length,
109 double position) noexcept {
110 const auto base = static_cast<long long>(std::floor(position));
111 const double frac = position - static_cast<double>(base);
112 const auto last = static_cast<long long>(length) - 1;
113
114 auto at = [&](long long index) -> double {
115 const long long clamped = std::clamp(index, 0LL, last);
116 return static_cast<double>(buffer[static_cast<std::size_t>(clamped)]);
117 };
118
119 const double s0 = at(base - 1);
120 const double s1 = at(base);
121 const double s2 = at(base + 1);
122 const double s3 = at(base + 2);
123
124 const double d0 = frac + 1.0;
125 const double d1 = frac;
126 const double d2 = frac - 1.0;
127 const double d3 = frac - 2.0;
128
129 return (-d1 * d2 * d3 / 6.0) * s0 + (d0 * d2 * d3 * 0.5) * s1 +
130 (-d0 * d1 * d3 * 0.5) * s2 + (d0 * d1 * d2 / 6.0) * s3;
131 }
132
133 std::vector<float> capture_;
134 std::vector<float> playback_;
135 std::size_t capacity_ = minimum_capacity_samples();
136 std::size_t position_ = 0;
137 std::size_t capture_length_ = 1;
138 std::size_t playback_length_ = 1;
139};
140
141} // namespace pulp::signal::chardelay
double process(double x, double segment_samples, double mod_offset_samples) noexcept
One sample of capture-forward / play-backward.
Definition reverse.hpp:70
static constexpr std::size_t minimum_capacity_samples() noexcept
Smallest capacity this segmenter can be prepared with.
Definition reverse.hpp:42
void reset() noexcept
Definition reverse.hpp:58
void prepare(std::size_t capacity_samples)
Allocates both buffers.
Definition reverse.hpp:51
Definition bbd.hpp:36
constexpr double kPi
Definition primitives.hpp:38
constexpr int kReverseFadeSamples
Raised-cosine splice fade at each end of a reversed segment, in samples.
Definition tables.hpp:331
@ buffer
A borrowed static buffer with a traversable playhead.
Definition granular.hpp:110
@ nearest
Snap to the nearest in-scale degree in either direction.
Definition harmony_engine.hpp:175