Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
harmony.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <array>
6#include <bit>
7#include <compare>
8#include <cstddef>
9#include <cstdint>
10#include <optional>
11#include <span>
12
13namespace pulp::music {
14
18 std::uint8_t scale_degree = 0;
19
20 constexpr auto operator<=>(const DiatonicChord&) const = default;
21};
22
23constexpr std::optional<DiatonicChord> diatonic_chord(const Scale& scale, std::size_t degree,
24 std::size_t tone_count = 3) noexcept {
25 if (degree >= scale.degree_count() || tone_count < 2 || tone_count > ChordFormula::kMaxNotes)
26 return std::nullopt;
27 const auto root = scale.degree_pitch_class(degree);
28 const auto root_offset = scale.degree_to_semitones(static_cast<int>(degree));
29 if (!root || !root_offset)
30 return std::nullopt;
31
32 std::array<int, ChordFormula::kMaxNotes> intervals{};
33 for (std::size_t tone = 0; tone < tone_count; ++tone) {
34 const auto offset =
35 scale.degree_to_semitones(static_cast<int>(degree) + static_cast<int>(tone * 2));
36 if (!offset)
37 return std::nullopt;
38 intervals[tone] = *offset - *root_offset;
39 }
40 const auto formula =
41 ChordFormula::from_intervals(std::span<const int>(intervals.data(), tone_count));
42 if (!formula)
43 return std::nullopt;
44 return DiatonicChord{*root, *formula, static_cast<std::uint8_t>(degree)};
45}
46
48 std::uint8_t matched = 0;
49 std::uint8_t missing = 0;
50 std::uint8_t extra = 0;
51 bool root_present = false;
52
53 constexpr bool exact() const noexcept {
54 return missing == 0 && extra == 0;
55 }
56 constexpr auto operator<=>(const RecognitionScore&) const = default;
57};
58
63 std::optional<std::uint8_t> inversion;
64 std::uint8_t inversion_match_count = 0;
65 std::uint8_t inversion_match_mask = 0;
66
67 constexpr auto operator<=>(const ChordRecognition&) const = default;
68};
69
71 public:
73
74 constexpr std::size_t size() const noexcept {
75 return count_;
76 }
77 constexpr std::size_t best_equivalent_count() const noexcept {
78 return best_equivalent_count_;
79 }
80 constexpr bool ambiguous() const noexcept {
81 return best_equivalent_count_ > 1;
82 }
83
84 constexpr std::optional<ChordRecognition> candidate(std::size_t index) const noexcept {
85 if (index >= count_)
86 return std::nullopt;
87 return candidates_[index];
88 }
89
90 private:
91 friend constexpr std::optional<ChordRecognitionList>
92 recognize_chord(PitchClassSet, std::optional<PitchClass>) noexcept;
93
94 std::array<ChordRecognition, kMaxCandidates> candidates_{};
95 std::uint16_t count_ = 0;
96 std::uint16_t best_equivalent_count_ = 0;
97};
98
99namespace detail {
100
101constexpr bool recognition_better(const ChordRecognition& lhs,
102 const ChordRecognition& rhs) noexcept {
103 const int lhs_distance = lhs.score.missing + lhs.score.extra;
104 const int rhs_distance = rhs.score.missing + rhs.score.extra;
105 if (lhs_distance != rhs_distance)
106 return lhs_distance < rhs_distance;
107 if (lhs.score.missing != rhs.score.missing)
108 return lhs.score.missing < rhs.score.missing;
109 if (lhs.score.extra != rhs.score.extra)
110 return lhs.score.extra < rhs.score.extra;
111 if (lhs.score.root_present != rhs.score.root_present)
112 return lhs.score.root_present;
113 const auto inversion_rank = [](const ChordRecognition& candidate) constexpr {
114 if (candidate.inversion_match_count == 1)
115 return 2;
116 if (candidate.inversion_match_count > 1)
117 return 1;
118 return 0;
119 };
120 if (inversion_rank(lhs) != inversion_rank(rhs))
121 return inversion_rank(lhs) > inversion_rank(rhs);
122 if (lhs.inversion_match_count != rhs.inversion_match_count)
123 return lhs.inversion_match_count < rhs.inversion_match_count;
124 if (lhs.root != rhs.root)
125 return static_cast<unsigned>(lhs.root) < static_cast<unsigned>(rhs.root);
126 return static_cast<unsigned>(lhs.quality) < static_cast<unsigned>(rhs.quality);
127}
128
129constexpr bool recognition_equivalent(const ChordRecognition& lhs,
130 const ChordRecognition& rhs) noexcept {
131 return lhs.score.missing == rhs.score.missing && lhs.score.extra == rhs.score.extra &&
132 lhs.score.root_present == rhs.score.root_present &&
133 lhs.inversion_match_count == rhs.inversion_match_count;
134}
135
136} // namespace detail
137
138constexpr std::optional<ChordRecognitionList>
140 std::optional<PitchClass> bass = std::nullopt) noexcept {
141 if (pitch_classes.empty() ||
142 (bass && (!is_valid(*bass) || !pitch_classes.contains(*bass))))
143 return std::nullopt;
144
146 for (int root = 0; root < kPitchClassesPerOctave; ++root) {
147 for (std::size_t quality_index = 0; quality_index < kChordQualityCount; ++quality_index) {
148 const auto quality = static_cast<ChordQuality>(quality_index);
149 const auto formula = ChordFormula::for_quality(quality);
150 const auto candidate_set = formula->pitch_classes().transposed(root);
151 const auto common =
152 static_cast<std::uint16_t>(candidate_set.mask() & pitch_classes.mask());
153 const auto missing =
154 static_cast<std::uint16_t>(candidate_set.mask() & ~pitch_classes.mask());
155 const auto extra =
156 static_cast<std::uint16_t>(pitch_classes.mask() & ~candidate_set.mask());
157
159 candidate.root = static_cast<PitchClass>(root);
160 candidate.quality = quality;
161 candidate.score =
162 RecognitionScore{static_cast<std::uint8_t>(std::popcount(common)),
163 static_cast<std::uint8_t>(std::popcount(missing)),
164 static_cast<std::uint8_t>(std::popcount(extra)),
165 pitch_classes.contains(static_cast<PitchClass>(root))};
166 if (bass) {
167 for (std::size_t inversion = 0; inversion < formula->size(); ++inversion) {
168 if (transpose(static_cast<PitchClass>(root), *formula->interval(inversion)) ==
169 *bass) {
170 candidate.inversion_match_mask = static_cast<std::uint8_t>(
171 candidate.inversion_match_mask | (1u << inversion));
172 ++candidate.inversion_match_count;
173 }
174 }
175 if (candidate.inversion_match_count == 1)
176 candidate.inversion =
177 static_cast<std::uint8_t>(std::countr_zero(candidate.inversion_match_mask));
178 }
179
180 std::size_t insert = result.count_;
181 while (insert > 0 &&
182 detail::recognition_better(candidate, result.candidates_[insert - 1])) {
183 result.candidates_[insert] = result.candidates_[insert - 1];
184 --insert;
185 }
186 result.candidates_[insert] = candidate;
187 ++result.count_;
188 }
189 }
190 result.best_equivalent_count_ = 1;
191 while (result.best_equivalent_count_ < result.count_ &&
192 detail::recognition_equivalent(result.candidates_[0],
193 result.candidates_[result.best_equivalent_count_]))
194 ++result.best_equivalent_count_;
195 return result;
196}
197
198constexpr std::optional<ChordRecognitionList>
199recognize_chord(std::span<const int> midi_pitches) noexcept {
200 if (midi_pitches.empty() || midi_pitches.size() > ChordFormula::kMaxNotes)
201 return std::nullopt;
202 std::uint16_t mask = 0;
203 int bass = 128;
204 for (const int pitch : midi_pitches) {
205 if (pitch < 0 || pitch > 127)
206 return std::nullopt;
207 bass = pitch < bass ? pitch : bass;
208 mask = static_cast<std::uint16_t>(mask |
209 (1u << static_cast<unsigned>(wrap_pitch_class(pitch))));
210 }
212}
213
214} // namespace pulp::music
Definition chord.hpp:87
static constexpr std::size_t kMaxNotes
Definition chord.hpp:89
static constexpr std::optional< ChordFormula > for_quality(ChordQuality quality) noexcept
Definition chord.hpp:110
static constexpr std::optional< ChordFormula > from_intervals(std::span< const int > intervals) noexcept
Definition chord.hpp:93
Definition harmony.hpp:70
constexpr bool ambiguous() const noexcept
Definition harmony.hpp:80
constexpr std::size_t size() const noexcept
Definition harmony.hpp:74
constexpr std::optional< ChordRecognition > candidate(std::size_t index) const noexcept
Definition harmony.hpp:84
friend constexpr std::optional< ChordRecognitionList > recognize_chord(PitchClassSet, std::optional< PitchClass >) noexcept
Definition harmony.hpp:139
constexpr std::size_t best_equivalent_count() const noexcept
Definition harmony.hpp:77
static constexpr std::size_t kMaxCandidates
Definition harmony.hpp:72
Definition pitch.hpp:56
static constexpr std::optional< PitchClassSet > from_mask(std::uint16_t mask) noexcept
Definition pitch.hpp:60
Definition pitch.hpp:266
Definition harmony.hpp:99
constexpr bool recognition_equivalent(const ChordRecognition &lhs, const ChordRecognition &rhs) noexcept
Definition harmony.hpp:129
constexpr bool recognition_better(const ChordRecognition &lhs, const ChordRecognition &rhs) noexcept
Definition harmony.hpp:101
Definition chord.hpp:14
constexpr bool is_valid(PitchClass pitch_class) noexcept
Definition pitch.hpp:34
constexpr std::size_t kChordQualityCount
Definition chord.hpp:31
constexpr PitchClass transpose(PitchClass pitch_class, int semitones) noexcept
Definition pitch.hpp:51
constexpr std::optional< ChordRecognitionList > recognize_chord(PitchClassSet pitch_classes, std::optional< PitchClass > bass=std::nullopt) noexcept
Definition harmony.hpp:139
constexpr std::optional< DiatonicChord > diatonic_chord(const Scale &scale, std::size_t degree, std::size_t tone_count=3) noexcept
Definition harmony.hpp:23
constexpr PitchClass wrap_pitch_class(int index) noexcept
Definition pitch.hpp:44
constexpr int kPitchClassesPerOctave
Definition pitch.hpp:16
PitchClass
Definition pitch.hpp:19
@ c
Definition pitch.hpp:20
ChordQuality
Definition chord.hpp:16
@ major
Definition chord.hpp:17
Definition harmony.hpp:59
PitchClass root
Definition harmony.hpp:60
std::uint8_t inversion_match_count
Definition harmony.hpp:64
constexpr auto operator<=>(const ChordRecognition &) const =default
std::uint8_t inversion_match_mask
Definition harmony.hpp:65
RecognitionScore score
Definition harmony.hpp:62
std::optional< std::uint8_t > inversion
Definition harmony.hpp:63
ChordQuality quality
Definition harmony.hpp:61
Definition harmony.hpp:15
constexpr auto operator<=>(const DiatonicChord &) const =default
std::uint8_t scale_degree
Definition harmony.hpp:18
ChordFormula formula
Definition harmony.hpp:17
PitchClass root
Definition harmony.hpp:16
Definition harmony.hpp:47
std::uint8_t extra
Definition harmony.hpp:50
std::uint8_t matched
Definition harmony.hpp:48
constexpr auto operator<=>(const RecognitionScore &) const =default
bool root_present
Definition harmony.hpp:51
constexpr bool exact() const noexcept
Definition harmony.hpp:53
std::uint8_t missing
Definition harmony.hpp:49