Sound Byte Libs 29c5ff3
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
segment.hpp
Go to the documentation of this file.
1// sbl/dsp/segment.hpp — Multi-segment envelope generator (float)
2//
3// SegmentGenerator advances through a sequence of timed segments, each
4// interpolating between a start level and a target level with a curve-shaped
5// phase. An optional sustain index causes the generator to hold after that
6// segment completes, resuming on gate_off().
7//
8// The curve engine is selected at compile time via template parameter —
9// zero runtime overhead, no vtables.
10//
11// All values are float: levels [0.0, 1.0], curve [-1.0, 1.0], time in ms.
12//
13// See FDP-014 and FDP-016 for design rationale.
14
15#pragma once
16
17#include <cstdint>
18
19#include <sbl/dsp/warp.hpp>
20
21namespace sbl::dsp {
22
23// ─── Segment ──────────────────────────────────────────────────────────
24
25struct Segment {
26 float target; // Target level [0.0, 1.0]
27 float curve; // Shape: -1.0 = concave, 0.0 = linear, 1.0 = convex
28 float time_ms; // Duration in milliseconds (0 = instant)
29};
30
31// ─── State ────────────────────────────────────────────────────────────
32
33enum class SegmentState : uint8_t {
34 Idle, // Output held at 0, waiting for gate_on
35 Running, // Advancing through a timed segment
36 Sustain, // Holding level until gate_off
37 Complete // All segments finished, output held at final level
38};
39
40// ─── SegmentGenerator ─────────────────────────────────────────────────
41//
42// Template on CurveEngine for compile-time selection of curve shaping.
43// Default is FloatRationalWarp (zero flash, analytical, good character).
44
45template<typename CurveEngine = FloatDefaultWarp>
47public:
48 /// @note All public methods are ISR-safe — bounded computation, no I/O.
49
50 static constexpr uint8_t MAX_SEGMENTS = 8;
51
52 // ── Configuration ─────────────────────────────────────────────
53
54 /// Configure the segment sequence.
55 /// @param segments Array of segments (copied internally, up to MAX_SEGMENTS)
56 /// @param count Number of segments
57 /// @param sustain_index Segment index to hold at (-1 = no sustain)
58 void configure(const Segment* segments, uint8_t count, int8_t sustain_index = -1) {
59 segment_count_ = (count > MAX_SEGMENTS) ? MAX_SEGMENTS : count;
60 for (uint8_t i = 0; i < segment_count_; ++i) {
61 segments_[i] = segments[i];
62 }
63 sustain_index_ = sustain_index;
64 reset();
65 }
66
67 void set_sample_rate(uint32_t sr) { sample_rate_ = static_cast<float>(sr); }
68
69 // ── Gate control ──────────────────────────────────────────────
70
71 /// Start the envelope from segment 0. If already running, restarts
72 /// from the current level (retrigger behavior).
73 void gate_on() {
74 if (segment_count_ == 0) return;
75 start_level_ = (state_ == SegmentState::Idle) ? 0.0f : level_;
76 state_ = SegmentState::Running;
77 begin_segment(0);
78 }
79
80 /// Release the envelope. If in sustain, jumps to the next segment
81 /// (typically release). If running pre-sustain, jumps to release.
82 /// If no sustain configured, does nothing.
83 void gate_off() {
84 if (sustain_index_ < 0) return;
85
86 uint8_t release_index = static_cast<uint8_t>(sustain_index_ + 1);
87 if (release_index >= segment_count_) return;
88
89 if (state_ == SegmentState::Sustain ||
90 (state_ == SegmentState::Running && current_index_ <= sustain_index_)) {
91 start_level_ = level_;
92 state_ = SegmentState::Running;
93 begin_segment(release_index);
94 }
95 }
96
97 /// Retrigger: restart from current level without requiring gate_off first.
98 void retrigger() {
99 if (segment_count_ == 0) return;
100 start_level_ = level_;
101 state_ = SegmentState::Running;
102 begin_segment(0);
103 }
104
105 // ── Processing ────────────────────────────────────────────────
106
107 /// Fill output buffer with envelope levels (float [0.0, 1.0]).
108 void process(float* out, uint16_t frames) {
109 for (uint16_t i = 0; i < frames; ++i) {
110 if (state_ == SegmentState::Running) {
111 float next_phase = phase_ + phase_increment_;
112 if (next_phase >= 1.0f) {
113 // Segment complete — snap to target
114 level_ = segments_[current_index_].target;
115 start_level_ = level_;
116 advance_to_next();
117 } else {
118 phase_ = next_phase;
119 level_ = compute_level(phase_);
120 }
121 }
122 out[i] = level_;
123 }
124 }
125
126 // ── Queries ───────────────────────────────────────────────────
127
128 float level() const { return level_; }
129
130 bool active() const {
131 return state_ != SegmentState::Idle && state_ != SegmentState::Complete;
132 }
133
134 SegmentState state() const { return state_; }
135 uint8_t current_segment_index() const { return current_index_; }
136
137private:
138 // ── Segment storage ───────────────────────────────────────────
139 Segment segments_[MAX_SEGMENTS] = {};
140 uint8_t segment_count_ = 0;
141 int8_t sustain_index_ = -1;
142
143 // ── Runtime state ─────────────────────────────────────────────
144 uint8_t current_index_ = 0;
146 float level_ = 0.0f;
147 float start_level_ = 0.0f;
148 float phase_ = 0.0f;
149 float phase_increment_ = 0.0f;
150 float sample_rate_ = 48000.0f;
151
152 // ── Internal helpers ──────────────────────────────────────────
153
154 void reset() {
155 state_ = SegmentState::Idle;
156 level_ = 0.0f;
157 start_level_ = 0.0f;
158 current_index_ = 0;
159 phase_ = 0.0f;
160 phase_increment_ = 0.0f;
161 }
162
163 void begin_segment(uint8_t index) {
164 current_index_ = index;
165 phase_ = 0.0f;
166
167 // Instant segment: snap to target and advance
168 if (segments_[index].time_ms <= 0.0f) {
169 level_ = segments_[index].target;
170 start_level_ = level_;
171 advance_to_next();
172 return;
173 }
174
175 float time_samples = segments_[index].time_ms * 0.001f * sample_rate_;
176 phase_increment_ = 1.0f / time_samples;
177 }
178
179 void advance_to_next() {
180 // Check for sustain hold
181 if (current_index_ == sustain_index_) {
182 state_ = SegmentState::Sustain;
183 return;
184 }
185
186 // Move to next segment
187 uint8_t next = current_index_ + 1;
188 if (next >= segment_count_) {
189 state_ = SegmentState::Complete;
190 return;
191 }
192
193 begin_segment(next);
194 }
195
196 float compute_level(float phase) const {
197 float warped = CurveEngine::warp(phase, segments_[current_index_].curve);
198 return start_level_ + (segments_[current_index_].target - start_level_) * warped;
199 }
200};
201
202} // namespace sbl::dsp
void set_sample_rate(uint32_t sr)
Definition segment.hpp:67
void configure(const Segment *segments, uint8_t count, int8_t sustain_index=-1)
Definition segment.hpp:58
void process(float *out, uint16_t frames)
Fill output buffer with envelope levels (float [0.0, 1.0]).
Definition segment.hpp:108
void retrigger()
Retrigger: restart from current level without requiring gate_off first.
Definition segment.hpp:98
uint8_t current_segment_index() const
Definition segment.hpp:135
SegmentState state() const
Definition segment.hpp:134
static constexpr uint8_t MAX_SEGMENTS
Definition segment.hpp:50
DSP atoms for audio signal processing.
Definition allpass.hpp:22