Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
voicing.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <algorithm>
6#include <array>
7#include <bit>
8#include <compare>
9#include <cstddef>
10#include <cstdint>
11#include <cstdlib>
12#include <limits>
13#include <optional>
14#include <span>
15
16namespace pulp::music {
17
18enum class VoicingMode : std::uint8_t {
19 closed = 0,
20 open = 1,
21 drop2 = 2,
22 drop3 = 3,
23 spread = 4,
24};
25
26struct MidiRange {
27 int lowest = 0;
28 int highest = 127;
29
30 constexpr bool valid() const noexcept {
31 return lowest >= 0 && highest <= 127 && lowest <= highest;
32 }
33
34 constexpr auto operator<=>(const MidiRange&) const = default;
35};
36
40 std::size_t inversion = 0;
42 int maximum_spacing = 127;
43
44 constexpr bool valid() const noexcept {
45 return static_cast<unsigned>(mode) <= static_cast<unsigned>(VoicingMode::spread) &&
47 maximum_spacing <= 127;
48 }
49};
50
51namespace detail {
52struct VoicingAccess;
53}
54
55class Voicing {
56 public:
57 constexpr std::size_t size() const noexcept {
58 return count_;
59 }
60
61 constexpr std::optional<int> pitch(std::size_t index) const noexcept {
62 if (index >= count_)
63 return std::nullopt;
64 return pitches_[index];
65 }
66
67 constexpr std::span<const std::uint8_t> pitches() const noexcept {
68 return {pitches_.data(), count_};
69 }
70
71 constexpr int motion() const noexcept {
72 return motion_;
73 }
74 constexpr auto operator<=>(const Voicing&) const = default;
75
76 private:
77 friend std::optional<Voicing> voice_chord(int, const ChordFormula&,
78 VoicingConstraints) noexcept;
79 friend std::optional<Voicing> minimum_motion_voice_leading(std::span<const int>, PitchClass,
80 const ChordFormula&,
81 MidiRange) noexcept;
82 friend struct detail::VoicingAccess;
83
84 std::array<std::uint8_t, ChordFormula::kMaxNotes> pitches_{};
85 std::uint8_t count_ = 0;
86 int motion_ = 0;
87};
88
89namespace detail {
90
92 static Voicing make(std::array<std::uint8_t, ChordFormula::kMaxNotes> pitches,
93 std::size_t count, int motion) noexcept {
94 Voicing result;
95 result.pitches_ = pitches;
96 result.count_ = static_cast<std::uint8_t>(count);
97 result.motion_ = motion;
98 return result;
99 }
100};
101
102inline std::optional<Voicing> solve_ordered_voicing(std::span<const PitchClass> pitch_classes,
103 std::span<const int> targets, MidiRange range,
104 int minimum_spacing,
105 int maximum_spacing) noexcept {
106 if (pitch_classes.empty() || pitch_classes.size() > ChordFormula::kMaxNotes ||
107 pitch_classes.size() != targets.size() || !range.valid() || minimum_spacing < 1 ||
108 maximum_spacing < minimum_spacing || maximum_spacing > 127)
109 return std::nullopt;
110
111 constexpr int unreachable = std::numeric_limits<int>::max() / 4;
112 std::array<std::array<int, 129>, ChordFormula::kMaxNotes + 1> cost{};
113 for (auto& row : cost)
114 row.fill(unreachable);
115 for (int previous = -1; previous <= 127; ++previous)
116 cost[pitch_classes.size()][static_cast<std::size_t>(previous + 1)] = 0;
117
118 for (std::size_t reverse = pitch_classes.size(); reverse-- > 0;) {
119 for (int previous = -1; previous <= 127; ++previous) {
120 int best = unreachable;
121 const int first = previous < 0 ? range.lowest : previous + minimum_spacing;
122 const int last =
123 previous < 0 ? range.highest : std::min(range.highest, previous + maximum_spacing);
124 for (int pitch = first; pitch <= last; ++pitch) {
125 if (wrap_pitch_class(pitch) != pitch_classes[reverse])
126 continue;
127 const int suffix = cost[reverse + 1][static_cast<std::size_t>(pitch + 1)];
128 if (suffix == unreachable)
129 continue;
130 const int local = std::abs(pitch - targets[reverse]);
131 best = std::min(best, local + suffix);
132 }
133 cost[reverse][static_cast<std::size_t>(previous + 1)] = best;
134 }
135 }
136
137 const int total = cost[0][0];
138 if (total == unreachable)
139 return std::nullopt;
140 std::array<std::uint8_t, ChordFormula::kMaxNotes> result{};
141 int previous = -1;
142 for (std::size_t note = 0; note < pitch_classes.size(); ++note) {
143 const int wanted = cost[note][static_cast<std::size_t>(previous + 1)];
144 const int first = previous < 0 ? range.lowest : previous + minimum_spacing;
145 const int last =
146 previous < 0 ? range.highest : std::min(range.highest, previous + maximum_spacing);
147 for (int pitch = first; pitch <= last; ++pitch) {
148 if (wrap_pitch_class(pitch) != pitch_classes[note])
149 continue;
150 const int suffix = cost[note + 1][static_cast<std::size_t>(pitch + 1)];
151 if (suffix != unreachable && std::abs(pitch - targets[note]) + suffix == wanted) {
152 result[note] = static_cast<std::uint8_t>(pitch);
153 previous = pitch;
154 break;
155 }
156 }
157 }
158 return VoicingAccess::make(result, pitch_classes.size(), total);
159}
160
161inline std::optional<Voicing>
162solve_unordered_voice_leading(std::span<const PitchClass> pitch_classes,
163 std::span<const int> previous, MidiRange range) noexcept {
164 if (pitch_classes.empty() || pitch_classes.size() > ChordFormula::kMaxNotes ||
165 pitch_classes.size() != previous.size() || !range.valid())
166 return std::nullopt;
167
168 constexpr std::uint16_t unreachable = std::numeric_limits<std::uint16_t>::max();
169 constexpr std::size_t state_count = 1u << ChordFormula::kMaxNotes;
170 std::array<std::array<std::uint16_t, 129>, state_count> cost{};
171 for (auto& row : cost)
172 row.fill(unreachable);
173
174 const auto full_mask = static_cast<std::size_t>((1u << pitch_classes.size()) - 1u);
175 cost[full_mask].fill(0);
176 for (std::size_t mask = full_mask; mask-- > 0;) {
177 const auto voice = static_cast<std::size_t>(std::popcount(mask));
178 for (int last_pitch = -1; last_pitch <= 127; ++last_pitch) {
179 std::uint16_t best = unreachable;
180 const int first = last_pitch < 0 ? range.lowest : last_pitch + 1;
181 for (std::size_t tone = 0; tone < pitch_classes.size(); ++tone) {
182 const auto tone_bit = static_cast<std::size_t>(1u << tone);
183 if ((mask & tone_bit) != 0)
184 continue;
185 for (int pitch = first; pitch <= range.highest; ++pitch) {
186 if (wrap_pitch_class(pitch) != pitch_classes[tone])
187 continue;
188 const auto suffix = cost[mask | tone_bit][static_cast<std::size_t>(pitch + 1)];
189 if (suffix == unreachable)
190 continue;
191 const int candidate = std::abs(pitch - previous[voice]) + suffix;
192 if (candidate < best)
193 best = static_cast<std::uint16_t>(candidate);
194 }
195 }
196 cost[mask][static_cast<std::size_t>(last_pitch + 1)] = best;
197 }
198 }
199
200 if (cost[0][0] == unreachable)
201 return std::nullopt;
202 std::array<std::uint8_t, ChordFormula::kMaxNotes> result{};
203 std::size_t mask = 0;
204 int last_pitch = -1;
205 for (std::size_t voice = 0; voice < pitch_classes.size(); ++voice) {
206 const auto wanted = cost[mask][static_cast<std::size_t>(last_pitch + 1)];
207 const int first = last_pitch < 0 ? range.lowest : last_pitch + 1;
208 bool selected = false;
209 for (int pitch = first; pitch <= range.highest && !selected; ++pitch) {
210 for (std::size_t tone = 0; tone < pitch_classes.size(); ++tone) {
211 const auto tone_bit = static_cast<std::size_t>(1u << tone);
212 if ((mask & tone_bit) != 0 || wrap_pitch_class(pitch) != pitch_classes[tone])
213 continue;
214 const auto suffix = cost[mask | tone_bit][static_cast<std::size_t>(pitch + 1)];
215 if (suffix != unreachable && std::abs(pitch - previous[voice]) + suffix == wanted) {
216 result[voice] = static_cast<std::uint8_t>(pitch);
217 mask |= tone_bit;
218 last_pitch = pitch;
219 selected = true;
220 break;
221 }
222 }
223 }
224 }
225 return VoicingAccess::make(result, pitch_classes.size(), cost[0][0]);
226}
227
228inline void sort_parallel(std::array<int, ChordFormula::kMaxNotes>& pitches,
229 std::array<PitchClass, ChordFormula::kMaxNotes>& pitch_classes,
230 std::size_t count) noexcept {
231 for (std::size_t right = 1; right < count; ++right) {
232 const int pitch = pitches[right];
233 const PitchClass pitch_class = pitch_classes[right];
234 std::size_t left = right;
235 while (left > 0 && pitches[left - 1] > pitch) {
236 pitches[left] = pitches[left - 1];
237 pitch_classes[left] = pitch_classes[left - 1];
238 --left;
239 }
240 pitches[left] = pitch;
241 pitch_classes[left] = pitch_class;
242 }
243}
244
245} // namespace detail
246
247inline std::optional<Voicing> voice_chord(int root_midi, const ChordFormula& formula,
248 VoicingConstraints constraints = {}) noexcept {
249 if (!constraints.valid() || formula.size() == 0 || constraints.inversion >= formula.size())
250 return std::nullopt;
251 if (root_midi < 0 || root_midi > 127)
252 return std::nullopt;
253
254 std::array<int, ChordFormula::kMaxNotes> targets{};
255 std::array<PitchClass, ChordFormula::kMaxNotes> pitch_classes{};
256 int previous = -1;
257 for (std::size_t note = 0; note < formula.size(); ++note) {
258 const std::size_t source = (note + constraints.inversion) % formula.size();
259 targets[note] = root_midi + *formula.interval(source);
260 if (source < constraints.inversion)
261 while (targets[note] <= previous)
262 targets[note] += kPitchClassesPerOctave;
263 pitch_classes[note] = wrap_pitch_class(targets[note]);
264 previous = targets[note];
265 }
266
267 switch (constraints.mode) {
269 break;
271 for (std::size_t note = 1; note + 1 < formula.size(); note += 2)
272 targets[note] += 12;
273 break;
275 if (formula.size() < 2)
276 return std::nullopt;
277 targets[formula.size() - 2] -= 12;
278 break;
280 if (formula.size() < 3)
281 return std::nullopt;
282 targets[formula.size() - 3] -= 12;
283 break;
285 constraints.minimum_spacing = std::max(constraints.minimum_spacing, 5);
286 break;
287 }
288 detail::sort_parallel(targets, pitch_classes, formula.size());
290 std::span<const PitchClass>(pitch_classes.data(), formula.size()),
291 std::span<const int>(targets.data(), formula.size()), constraints.range,
292 constraints.minimum_spacing, constraints.maximum_spacing);
293}
294
295inline std::optional<Voicing> minimum_motion_voice_leading(std::span<const int> previous,
296 PitchClass target_root,
297 const ChordFormula& formula,
298 MidiRange range = {}) noexcept {
299 if (!is_valid(target_root) || !range.valid() || previous.empty() ||
300 previous.size() != formula.size() || previous.size() > ChordFormula::kMaxNotes)
301 return std::nullopt;
302 for (std::size_t voice = 0; voice < previous.size(); ++voice) {
303 if (previous[voice] < 0 || previous[voice] > 127 || !range.valid() ||
304 (voice != 0 && previous[voice - 1] >= previous[voice]))
305 return std::nullopt;
306 }
307
308 std::array<PitchClass, ChordFormula::kMaxNotes> pitch_classes{};
309 for (std::size_t tone = 0; tone < formula.size(); ++tone)
310 pitch_classes[tone] = transpose(target_root, *formula.interval(tone));
312 std::span<const PitchClass>(pitch_classes.data(), formula.size()), previous, range);
313}
314
315} // namespace pulp::music
Definition chord.hpp:87
constexpr std::optional< int > interval(std::size_t index) const noexcept
Definition chord.hpp:152
static constexpr std::size_t kMaxNotes
Definition chord.hpp:89
constexpr std::size_t size() const noexcept
Definition chord.hpp:149
Definition voicing.hpp:55
constexpr auto operator<=>(const Voicing &) const =default
friend std::optional< Voicing > minimum_motion_voice_leading(std::span< const int >, PitchClass, const ChordFormula &, MidiRange) noexcept
Definition voicing.hpp:295
constexpr std::span< const std::uint8_t > pitches() const noexcept
Definition voicing.hpp:67
constexpr int motion() const noexcept
Definition voicing.hpp:71
constexpr std::optional< int > pitch(std::size_t index) const noexcept
Definition voicing.hpp:61
friend std::optional< Voicing > voice_chord(int, const ChordFormula &, VoicingConstraints) noexcept
Definition voicing.hpp:247
constexpr std::size_t size() const noexcept
Definition voicing.hpp:57
std::optional< Voicing > solve_unordered_voice_leading(std::span< const PitchClass > pitch_classes, std::span< const int > previous, MidiRange range) noexcept
Definition voicing.hpp:162
std::optional< Voicing > solve_ordered_voicing(std::span< const PitchClass > pitch_classes, std::span< const int > targets, MidiRange range, int minimum_spacing, int maximum_spacing) noexcept
Definition voicing.hpp:102
void sort_parallel(std::array< int, ChordFormula::kMaxNotes > &pitches, std::array< PitchClass, ChordFormula::kMaxNotes > &pitch_classes, std::size_t count) noexcept
Definition voicing.hpp:228
Definition chord.hpp:14
std::optional< Voicing > voice_chord(int root_midi, const ChordFormula &formula, VoicingConstraints constraints={}) noexcept
Definition voicing.hpp:247
constexpr bool is_valid(PitchClass pitch_class) noexcept
Definition pitch.hpp:34
std::optional< Voicing > minimum_motion_voice_leading(std::span< const int > previous, PitchClass target_root, const ChordFormula &formula, MidiRange range={}) noexcept
Definition voicing.hpp:295
constexpr PitchClass transpose(PitchClass pitch_class, int semitones) noexcept
Definition pitch.hpp:51
@ reverse
Definition pattern.hpp:166
constexpr PitchClass wrap_pitch_class(int index) noexcept
Definition pitch.hpp:44
VoicingMode
Definition voicing.hpp:18
@ closed
Definition voicing.hpp:19
@ open
Definition voicing.hpp:20
@ drop3
Definition voicing.hpp:22
@ spread
Definition voicing.hpp:23
@ drop2
Definition voicing.hpp:21
constexpr int kPitchClassesPerOctave
Definition pitch.hpp:16
PitchClass
Definition pitch.hpp:19
Definition voicing.hpp:26
constexpr auto operator<=>(const MidiRange &) const =default
constexpr bool valid() const noexcept
Definition voicing.hpp:30
int lowest
Definition voicing.hpp:27
int highest
Definition voicing.hpp:28
Definition voicing.hpp:37
std::size_t inversion
Definition voicing.hpp:40
VoicingMode mode
Definition voicing.hpp:38
MidiRange range
Definition voicing.hpp:39
int maximum_spacing
Definition voicing.hpp:42
constexpr bool valid() const noexcept
Definition voicing.hpp:44
int minimum_spacing
Definition voicing.hpp:41
Definition voicing.hpp:91
static Voicing make(std::array< std::uint8_t, ChordFormula::kMaxNotes > pitches, std::size_t count, int motion) noexcept
Definition voicing.hpp:92