Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pitch.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <array>
4#include <bit>
5#include <compare>
6#include <cstddef>
7#include <cstdint>
8#include <initializer_list>
9#include <limits>
10#include <optional>
11#include <span>
12#include <string_view>
13
14namespace pulp::music {
15
16inline constexpr int kPitchClassesPerOctave = 12;
17inline constexpr std::uint16_t kPitchClassMask = 0x0FFFu;
18
19enum class PitchClass : std::uint8_t {
20 c = 0,
22 d = 2,
24 e = 4,
25 f = 5,
27 g = 7,
29 a = 9,
30 a_sharp = 10,
31 b = 11,
32};
33
34constexpr bool is_valid(PitchClass pitch_class) noexcept {
35 return static_cast<unsigned>(pitch_class) < kPitchClassesPerOctave;
36}
37
38constexpr std::optional<PitchClass> pitch_class_from_index(int index) noexcept {
39 if (index < 0 || index >= kPitchClassesPerOctave)
40 return std::nullopt;
41 return static_cast<PitchClass>(index);
42}
43
44constexpr PitchClass wrap_pitch_class(int index) noexcept {
45 int wrapped = index % kPitchClassesPerOctave;
46 if (wrapped < 0)
47 wrapped += kPitchClassesPerOctave;
48 return static_cast<PitchClass>(wrapped);
49}
50
51constexpr PitchClass transpose(PitchClass pitch_class, int semitones) noexcept {
52 return wrap_pitch_class(static_cast<int>(pitch_class) +
53 static_cast<int>(wrap_pitch_class(semitones)));
54}
55
57 public:
58 constexpr PitchClassSet() noexcept = default;
59
60 static constexpr std::optional<PitchClassSet> from_mask(std::uint16_t mask) noexcept {
61 if ((mask & ~kPitchClassMask) != 0)
62 return std::nullopt;
63 return PitchClassSet(mask, TrustedMask{});
64 }
65
66 static constexpr std::optional<PitchClassSet>
67 from_pitch_classes(std::span<const PitchClass> pitch_classes) noexcept {
68 std::uint16_t mask = 0;
69 for (const auto pitch_class : pitch_classes) {
70 if (!is_valid(pitch_class))
71 return std::nullopt;
72 mask = static_cast<std::uint16_t>(mask | (1u << static_cast<unsigned>(pitch_class)));
73 }
74 return PitchClassSet(mask, TrustedMask{});
75 }
76
77 constexpr std::uint16_t mask() const noexcept {
78 return mask_;
79 }
80 constexpr bool empty() const noexcept {
81 return mask_ == 0;
82 }
83 constexpr std::size_t size() const noexcept {
84 return std::popcount(mask_);
85 }
86
87 constexpr bool contains(PitchClass pitch_class) const noexcept {
88 return is_valid(pitch_class) && ((mask_ >> static_cast<unsigned>(pitch_class)) & 1u) != 0;
89 }
90
91 constexpr std::optional<PitchClass> at(std::size_t index) const noexcept {
92 std::size_t seen = 0;
93 for (int pitch = 0; pitch < kPitchClassesPerOctave; ++pitch) {
94 const auto pitch_class = static_cast<PitchClass>(pitch);
95 if (!contains(pitch_class))
96 continue;
97 if (seen == index)
98 return pitch_class;
99 ++seen;
100 }
101 return std::nullopt;
102 }
103
104 constexpr std::optional<std::size_t> index_of(PitchClass pitch_class) const noexcept {
105 if (!contains(pitch_class))
106 return std::nullopt;
107 std::size_t index = 0;
108 for (int pitch = 0; pitch < static_cast<int>(pitch_class); ++pitch)
109 if ((mask_ & (1u << pitch)) != 0)
110 ++index;
111 return index;
112 }
113
114 constexpr PitchClassSet transposed(int semitones) const noexcept {
115 std::uint16_t result = 0;
116 for (int pitch = 0; pitch < kPitchClassesPerOctave; ++pitch) {
117 if ((mask_ & (1u << pitch)) == 0)
118 continue;
119 result = static_cast<std::uint16_t>(
120 result | (1u << static_cast<unsigned>(
121 transpose(static_cast<PitchClass>(pitch), semitones))));
122 }
123 return PitchClassSet(result, TrustedMask{});
124 }
125
126 constexpr PitchClassSet inverted(PitchClass axis = PitchClass::c) const noexcept {
127 std::uint16_t result = 0;
128 const int center = 2 * static_cast<int>(axis);
129 for (int pitch = 0; pitch < kPitchClassesPerOctave; ++pitch) {
130 if ((mask_ & (1u << pitch)) == 0)
131 continue;
132 result = static_cast<std::uint16_t>(
133 result | (1u << static_cast<unsigned>(wrap_pitch_class(center - pitch))));
134 }
135 return PitchClassSet(result, TrustedMask{});
136 }
137
138 constexpr auto operator<=>(const PitchClassSet&) const = default;
139
140 private:
141 struct TrustedMask {};
142 constexpr PitchClassSet(std::uint16_t mask, TrustedMask) noexcept : mask_(mask) {}
143
144 std::uint16_t mask_ = 0;
145};
146
163
164inline constexpr std::size_t kNamedScaleCount = 14;
165
166namespace detail {
167
168constexpr std::uint16_t pitch_class_mask(std::initializer_list<int> semitones) noexcept {
169 std::uint16_t mask = 0;
170 for (const int semitone : semitones)
171 if (semitone >= 0 && semitone < kPitchClassesPerOctave)
172 mask = static_cast<std::uint16_t>(mask | (1u << semitone));
173 return mask;
174}
175
176} // namespace detail
177
182
183// These indices match signal::ScaleType and its stored selector domain.
184inline constexpr std::array<ScaleCompatibilityEntry, 10> kPulpSignalScales{{
185 {"major", NamedScale::major},
186 {"natural_minor", NamedScale::natural_minor},
187 {"dorian", NamedScale::dorian},
188 {"phrygian", NamedScale::phrygian},
189 {"lydian", NamedScale::lydian},
190 {"mixolydian", NamedScale::mixolydian},
191 {"harmonic_minor", NamedScale::harmonic_minor},
192 {"melodic_minor", NamedScale::melodic_minor},
193 {"major_pentatonic", NamedScale::major_pentatonic},
194 {"minor_pentatonic", NamedScale::minor_pentatonic},
195}};
196
197// These indices match timeline::ScaleMode and its persisted spelling.
198inline constexpr std::array<ScaleCompatibilityEntry, 10> kPulpTimelineScales{{
199 {"major", NamedScale::major},
200 {"natural_minor", NamedScale::natural_minor},
201 {"harmonic_minor", NamedScale::harmonic_minor},
202 {"melodic_minor", NamedScale::melodic_minor},
203 {"dorian", NamedScale::dorian},
204 {"phrygian", NamedScale::phrygian},
205 {"lydian", NamedScale::lydian},
206 {"mixolydian", NamedScale::mixolydian},
207 {"locrian", NamedScale::locrian},
208 {"chromatic", NamedScale::chromatic},
209}};
210
211// These indices match Forge's saved ten-scale transform selector.
212inline constexpr std::array<ScaleCompatibilityEntry, 10> kForgeRuntimeScales{{
213 {"major", NamedScale::major},
214 {"minor", NamedScale::natural_minor},
215 {"dorian", NamedScale::dorian},
216 {"phrygian", NamedScale::phrygian},
217 {"lydian", NamedScale::lydian},
218 {"mixolydian", NamedScale::mixolydian},
219 {"harmonic_minor", NamedScale::harmonic_minor},
220 {"pentatonic_major", NamedScale::major_pentatonic},
221 {"pentatonic_minor", NamedScale::minor_pentatonic},
222 {"chromatic", NamedScale::chromatic},
223}};
224
225// These indices match Forge's broader primitive scale table.
226inline constexpr std::array<ScaleCompatibilityEntry, 14> kForgePrimitiveScales{{
227 {"ionian", NamedScale::major},
228 {"dorian", NamedScale::dorian},
229 {"phrygian", NamedScale::phrygian},
230 {"lydian", NamedScale::lydian},
231 {"mixolydian", NamedScale::mixolydian},
232 {"aeolian", NamedScale::natural_minor},
233 {"locrian", NamedScale::locrian},
234 {"harmonic_minor", NamedScale::harmonic_minor},
235 {"melodic_minor", NamedScale::melodic_minor},
236 {"major_pentatonic", NamedScale::major_pentatonic},
237 {"minor_pentatonic", NamedScale::minor_pentatonic},
238 {"blues", NamedScale::blues},
239 {"whole_tone", NamedScale::whole_tone},
240 {"chromatic", NamedScale::chromatic},
241}};
242
243constexpr std::optional<PitchClassSet> scale_intervals(NamedScale scale) noexcept {
244 constexpr std::array<std::uint16_t, kNamedScaleCount> masks{{
245 detail::pitch_class_mask({0, 2, 4, 5, 7, 9, 11}),
246 detail::pitch_class_mask({0, 2, 3, 5, 7, 8, 10}),
247 detail::pitch_class_mask({0, 2, 3, 5, 7, 9, 10}),
248 detail::pitch_class_mask({0, 1, 3, 5, 7, 8, 10}),
249 detail::pitch_class_mask({0, 2, 4, 6, 7, 9, 11}),
250 detail::pitch_class_mask({0, 2, 4, 5, 7, 9, 10}),
251 detail::pitch_class_mask({0, 2, 3, 5, 7, 8, 11}),
252 detail::pitch_class_mask({0, 2, 3, 5, 7, 9, 11}),
253 detail::pitch_class_mask({0, 2, 4, 7, 9}),
254 detail::pitch_class_mask({0, 3, 5, 7, 10}),
255 detail::pitch_class_mask({0, 1, 3, 5, 6, 8, 10}),
256 detail::pitch_class_mask({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}),
257 detail::pitch_class_mask({0, 3, 5, 6, 7, 10}),
258 detail::pitch_class_mask({0, 2, 4, 6, 8, 10}),
259 }};
260 const auto index = static_cast<std::size_t>(scale);
261 if (index >= masks.size())
262 return std::nullopt;
263 return PitchClassSet::from_mask(masks[index]);
264}
265
266class Scale {
267 public:
268 static constexpr std::optional<Scale> from_intervals(PitchClass root,
269 PitchClassSet intervals) noexcept {
270 if (!is_valid(root))
271 return std::nullopt;
272 return Scale(root, intervals);
273 }
274
275 static constexpr std::optional<Scale> named(PitchClass root, NamedScale name) noexcept {
276 if (!is_valid(root))
277 return std::nullopt;
278 const auto intervals = scale_intervals(name);
279 if (!intervals)
280 return std::nullopt;
281 return Scale(root, *intervals);
282 }
283
284 constexpr PitchClass root() const noexcept {
285 return root_;
286 }
287 constexpr PitchClassSet intervals() const noexcept {
288 return intervals_;
289 }
290 constexpr PitchClassSet pitch_classes() const noexcept {
291 return intervals_.transposed(static_cast<int>(root_));
292 }
293 constexpr std::size_t degree_count() const noexcept {
294 return intervals_.size();
295 }
296
297 constexpr bool contains(PitchClass pitch_class) const noexcept {
298 return pitch_classes().contains(pitch_class);
299 }
300
301 constexpr std::optional<PitchClass> degree_pitch_class(std::size_t degree) const noexcept {
302 const auto interval = intervals_.at(degree);
303 if (!interval)
304 return std::nullopt;
305 return transpose(root_, static_cast<int>(*interval));
306 }
307
308 constexpr std::optional<std::size_t> degree_index(PitchClass pitch_class) const noexcept {
309 if (!is_valid(pitch_class))
310 return std::nullopt;
311 return intervals_.index_of(
312 wrap_pitch_class(static_cast<int>(pitch_class) - static_cast<int>(root_)));
313 }
314
315 constexpr std::optional<int> degree_to_semitones(int degree) const noexcept {
316 const int count = static_cast<int>(degree_count());
317 if (count == 0)
318 return std::nullopt;
319 int octave = degree / count;
320 int within = degree % count;
321 if (within < 0) {
322 within += count;
323 --octave;
324 }
325 const auto interval = intervals_.at(static_cast<std::size_t>(within));
326 const auto semitones = static_cast<std::int64_t>(octave) * kPitchClassesPerOctave +
327 static_cast<int>(*interval);
328 if (semitones < std::numeric_limits<int>::min() ||
329 semitones > std::numeric_limits<int>::max())
330 return std::nullopt;
331 return static_cast<int>(semitones);
332 }
333
334 constexpr Scale transposed(int semitones) const noexcept {
335 return Scale(transpose(root_, semitones), intervals_);
336 }
337
338 constexpr std::optional<Scale> rotated(std::size_t degree) const noexcept {
339 const auto pivot = intervals_.at(degree);
340 if (!pivot)
341 return std::nullopt;
342 const int semitones = static_cast<int>(*pivot);
343 return Scale(transpose(root_, semitones), intervals_.transposed(-semitones));
344 }
345
346 constexpr auto operator<=>(const Scale&) const = default;
347
348 private:
349 constexpr Scale(PitchClass root, PitchClassSet intervals) noexcept
350 : root_(root), intervals_(intervals) {}
351
353 PitchClassSet intervals_{};
354};
355
356} // namespace pulp::music
Definition pitch.hpp:56
constexpr std::uint16_t mask() const noexcept
Definition pitch.hpp:77
constexpr bool contains(PitchClass pitch_class) const noexcept
Definition pitch.hpp:87
constexpr PitchClassSet() noexcept=default
constexpr std::optional< std::size_t > index_of(PitchClass pitch_class) const noexcept
Definition pitch.hpp:104
constexpr auto operator<=>(const PitchClassSet &) const =default
static constexpr std::optional< PitchClassSet > from_mask(std::uint16_t mask) noexcept
Definition pitch.hpp:60
constexpr PitchClassSet inverted(PitchClass axis=PitchClass::c) const noexcept
Definition pitch.hpp:126
static constexpr std::optional< PitchClassSet > from_pitch_classes(std::span< const PitchClass > pitch_classes) noexcept
Definition pitch.hpp:67
constexpr std::size_t size() const noexcept
Definition pitch.hpp:83
constexpr PitchClassSet transposed(int semitones) const noexcept
Definition pitch.hpp:114
constexpr std::optional< PitchClass > at(std::size_t index) const noexcept
Definition pitch.hpp:91
constexpr bool empty() const noexcept
Definition pitch.hpp:80
static constexpr std::optional< Scale > from_intervals(PitchClass root, PitchClassSet intervals) noexcept
Definition pitch.hpp:268
constexpr std::optional< int > degree_to_semitones(int degree) const noexcept
Definition pitch.hpp:315
constexpr std::size_t degree_count() const noexcept
Definition pitch.hpp:293
constexpr Scale transposed(int semitones) const noexcept
Definition pitch.hpp:334
static constexpr std::optional< Scale > named(PitchClass root, NamedScale name) noexcept
Definition pitch.hpp:275
constexpr std::optional< Scale > rotated(std::size_t degree) const noexcept
Definition pitch.hpp:338
constexpr PitchClassSet intervals() const noexcept
Definition pitch.hpp:287
constexpr PitchClass root() const noexcept
Definition pitch.hpp:284
constexpr std::optional< std::size_t > degree_index(PitchClass pitch_class) const noexcept
Definition pitch.hpp:308
constexpr PitchClassSet pitch_classes() const noexcept
Definition pitch.hpp:290
constexpr bool contains(PitchClass pitch_class) const noexcept
Definition pitch.hpp:297
constexpr auto operator<=>(const Scale &) const =default
constexpr std::optional< PitchClass > degree_pitch_class(std::size_t degree) const noexcept
Definition pitch.hpp:301
Definition harmony.hpp:99
constexpr std::uint16_t pitch_class_mask(std::initializer_list< int > semitones) noexcept
Definition pitch.hpp:168
Definition chord.hpp:14
constexpr bool is_valid(PitchClass pitch_class) noexcept
Definition pitch.hpp:34
constexpr std::array< ScaleCompatibilityEntry, 14 > kForgePrimitiveScales
Definition pitch.hpp:226
constexpr std::array< ScaleCompatibilityEntry, 10 > kPulpSignalScales
Definition pitch.hpp:184
constexpr std::size_t kNamedScaleCount
Definition pitch.hpp:164
constexpr PitchClass transpose(PitchClass pitch_class, int semitones) noexcept
Definition pitch.hpp:51
constexpr std::array< ScaleCompatibilityEntry, 10 > kPulpTimelineScales
Definition pitch.hpp:198
NamedScale
Definition pitch.hpp:147
@ lydian
Definition pitch.hpp:152
@ minor_pentatonic
Definition pitch.hpp:157
@ natural_minor
Definition pitch.hpp:149
@ phrygian
Definition pitch.hpp:151
@ blues
Definition pitch.hpp:160
@ locrian
Definition pitch.hpp:158
@ melodic_minor
Definition pitch.hpp:155
@ whole_tone
Definition pitch.hpp:161
@ dorian
Definition pitch.hpp:150
@ chromatic
Definition pitch.hpp:159
@ mixolydian
Definition pitch.hpp:153
@ harmonic_minor
Definition pitch.hpp:154
@ major_pentatonic
Definition pitch.hpp:156
@ major
Definition pitch.hpp:148
constexpr std::array< ScaleCompatibilityEntry, 10 > kForgeRuntimeScales
Definition pitch.hpp:212
constexpr PitchClass wrap_pitch_class(int index) noexcept
Definition pitch.hpp:44
constexpr int kPitchClassesPerOctave
Definition pitch.hpp:16
PitchClass
Definition pitch.hpp:19
@ a
Definition pitch.hpp:29
@ c
Definition pitch.hpp:20
@ a_sharp
Definition pitch.hpp:30
@ d
Definition pitch.hpp:22
@ f
Definition pitch.hpp:25
@ d_sharp
Definition pitch.hpp:23
@ b
Definition pitch.hpp:31
@ g_sharp
Definition pitch.hpp:28
@ f_sharp
Definition pitch.hpp:26
@ g
Definition pitch.hpp:27
@ e
Definition pitch.hpp:24
@ c_sharp
Definition pitch.hpp:21
constexpr std::uint16_t kPitchClassMask
Definition pitch.hpp:17
constexpr std::optional< PitchClassSet > scale_intervals(NamedScale scale) noexcept
Definition pitch.hpp:243
constexpr std::optional< PitchClass > pitch_class_from_index(int index) noexcept
Definition pitch.hpp:38
@ octave
Definition chord.hpp:28
@ major
Definition chord.hpp:17
STL namespace.
Definition pitch.hpp:178
NamedScale scale
Definition pitch.hpp:180
std::string_view stored_name
Definition pitch.hpp:179