FastLED 3.9.15
Loading...
Searching...
No Matches
note.cpp.hpp
Go to the documentation of this file.
3#include "fl/math/math.h"
4#include "fl/stl/noexcept.h"
5
6namespace fl {
7namespace audio {
8namespace detector {
9
12 , mOwnsPitchDetector(true)
14 , mLastVelocity(0)
15 , mNoteActive(false)
16 , mCurrentPitch(0.0f)
17 , mPitchBend(0.0f)
18 , mNoteOnEnergy(0.0f)
19 , mNoteOnTime(0)
21 , mNoteOnThreshold(0.6f)
22 , mNoteOffThreshold(0.4f)
23 , mMinNoteDuration(50) // 50ms minimum note duration
24 , mNoteChangeThreshold(1) // 1 semitone threshold for note change
26{}
27
29 : mPitchDetector(pitchDetector)
30 , mOwnsPitchDetector(false)
32 , mLastVelocity(0)
33 , mNoteActive(false)
34 , mCurrentPitch(0.0f)
35 , mPitchBend(0.0f)
36 , mNoteOnEnergy(0.0f)
37 , mNoteOnTime(0)
39 , mNoteOnThreshold(0.6f)
40 , mNoteOffThreshold(0.4f)
44{}
45
46Note::~Note() FL_NOEXCEPT = default;
47
48void Note::update(shared_ptr<Context> context) {
49 // Update pitch detector if we own it
51 mPitchDetector->update(context);
52 }
53
54 // Get pitch detection results
55 if (!mPitchDetector) {
56 return; // No pitch detector available
57 }
58
59 float pitch = mPitchDetector->getPitch();
60 float confidence = mPitchDetector->getConfidence();
61 bool voiced = mPitchDetector->isVoiced();
62 u32 timestamp = context->getTimestamp();
63 float energy = context->getRMS();
64
65 mCurrentPitch = pitch;
66 mLastUpdateTime = timestamp;
67
68 // State machine: note-on, note-off, note-change detection
69 if (!mNoteActive) {
70 // No note currently active - check for note-on
71 if (shouldTriggerNoteOn(confidence, pitch)) {
72 u8 newNote = frequencyToMidiNote(pitch);
73 u8 velocity = calculateVelocity(energy, confidence);
74
75 mCurrentNote = newNote;
76 mLastVelocity = velocity;
77 mNoteActive = true;
78 mNoteOnTime = timestamp;
79 mNoteOnEnergy = energy;
80 mPitchBend = calculatePitchBend(pitch, newNote);
81
82 mFireNoteOn = true;
83 mPendingOnNote = newNote;
84 mPendingOnVelocity = velocity;
85 }
86 } else {
87 // Note currently active - check for note-off or note-change
88 if (shouldTriggerNoteOff(confidence, voiced)) {
89 // Check minimum note duration to prevent flicker
90 u32 noteDuration = timestamp - mNoteOnTime;
91 if (noteDuration >= mMinNoteDuration) {
92 mFireNoteOff = true;
94
96 mLastVelocity = 0;
97 mNoteActive = false;
98 mPitchBend = 0.0f;
99 }
100 } else if (voiced && confidence >= mNoteOnThreshold) {
101 // Note still active - check for note change
102 u8 newNote = frequencyToMidiNote(pitch);
104
106 u8 velocity = calculateVelocity(energy, confidence);
107
108 // Set pending note-off for old note
109 mFireNoteOff = true;
111
112 // Update to new note
113 mCurrentNote = newNote;
114 mLastVelocity = velocity;
115 mNoteOnTime = timestamp;
116 mPitchBend = calculatePitchBend(pitch, newNote);
117
118 // Set pending note-on for new note
119 mFireNoteOn = true;
120 mPendingOnNote = newNote;
121 mPendingOnVelocity = velocity;
122 mFireNoteChange = true;
123 }
124 }
125 }
126}
127
142
145 mLastVelocity = 0;
146 mNoteActive = false;
147 mCurrentPitch = 0.0f;
148 mPitchBend = 0.0f;
149 mNoteOnEnergy = 0.0f;
150 mNoteOnTime = 0;
151 mLastUpdateTime = 0;
152
154 mPitchDetector->reset();
155 }
156}
157
159 if (hz <= 0.0f) {
160 return NO_NOTE;
161 }
162
163 // MIDI note = 69 + 12 × log₂(f / 440)
164 float semitones = 12.0f * (fl::logf(hz / A4_FREQUENCY) / fl::logf(2.0f));
165 int midiNote = static_cast<int>(A4_MIDI_NOTE + semitones + 0.5f); // Round to nearest
166
167 // Clamp to valid MIDI range (0-127)
168 if (midiNote < 0) {
169 return 0;
170 }
171 if (midiNote > 127) {
172 return 127;
173 }
174
175 return static_cast<u8>(midiNote);
176}
177
178float Note::midiNoteToFrequency(u8 note) const {
179 if (note == NO_NOTE) {
180 return 0.0f;
181 }
182
183 // f = 440 × 2^((n - 69) / 12)
184 float semitones = static_cast<float>(note) - A4_MIDI_NOTE;
185 return A4_FREQUENCY * fl::powf(2.0f, semitones / 12.0f);
186}
187
188float Note::calculatePitchBend(float hz, u8 note) const {
189 if (note == NO_NOTE || hz <= 0.0f) {
190 return 0.0f;
191 }
192
193 float noteFrequency = midiNoteToFrequency(note);
194 if (noteFrequency <= 0.0f) {
195 return 0.0f;
196 }
197
198 // Calculate cents deviation from note center
199 // Cents = 1200 × log₂(f / f_note)
200 float cents = 1200.0f * (fl::logf(hz / noteFrequency) / fl::logf(2.0f));
201
202 // Clamp to ±50 cents (typical range within a single note)
203 return fl::clamp(cents, -50.0f, 50.0f);
204}
205
206u8 Note::calculateVelocity(float energy, float confidence) const {
207 // Velocity calculation based on RMS energy and pitch confidence
208 // Higher energy = higher velocity
209 // Higher confidence = more reliable velocity
210
211 // Normalize energy to 0-1 range (typical RMS values: 0.0-1.0)
212 float normalizedEnergy = fl::clamp(energy * mVelocitySensitivity, 0.0f, 1.0f);
213
214 // Weight by confidence (more confident = more accurate velocity)
215 float weightedVelocity = normalizedEnergy * confidence;
216
217 // Convert to MIDI velocity (1-127, 0 reserved for note-off)
218 int velocity = static_cast<int>(weightedVelocity * 126.0f) + 1;
219
220 return static_cast<u8>(fl::clamp(velocity, 1, 127));
221}
222
223bool Note::shouldTriggerNoteOn(float confidence, float pitch) const {
224 // Trigger note-on when:
225 // 1. Confidence exceeds threshold
226 // 2. Pitch is valid (non-zero)
227 return (confidence >= mNoteOnThreshold) && (pitch > 0.0f);
228}
229
230bool Note::shouldTriggerNoteOff(float confidence, bool voiced) const {
231 // Trigger note-off when:
232 // 1. Confidence drops below off threshold (hysteresis)
233 // 2. OR voice becomes unvoiced (silence or percussion)
234 return (confidence < mNoteOffThreshold) || !voiced;
235}
236
237bool Note::shouldTriggerNoteChange(u8 newNote, u8 currentNote) const {
238 if (newNote == NO_NOTE || currentNote == NO_NOTE) {
239 return false;
240 }
241
242 // Calculate semitone difference
243 int semitoneDistance = fl::abs(static_cast<int>(newNote) - static_cast<int>(currentNote));
244
245 // Trigger note change if difference exceeds threshold
246 return semitoneDistance >= mNoteChangeThreshold;
247}
248
249} // namespace detector
250} // namespace audio
251} // namespace fl
function_list< void(u8 note, u8 velocity)> onNoteOn
Definition note.h:45
float calculatePitchBend(float hz, u8 note) const
Definition note.cpp.hpp:188
void reset() override
Definition note.cpp.hpp:143
void fireCallbacks() override
Definition note.cpp.hpp:128
~Note() FL_NOEXCEPT override
static constexpr u8 A4_MIDI_NOTE
Definition note.h:109
float midiNoteToFrequency(u8 note) const
Definition note.cpp.hpp:178
bool shouldTriggerNoteOn(float confidence, float pitch) const
Definition note.cpp.hpp:223
bool shouldTriggerNoteOff(float confidence, bool voiced) const
Definition note.cpp.hpp:230
function_list< void(u8 note, u8 velocity)> onNoteChange
Definition note.h:47
function_list< void(u8 note)> onNoteOff
Definition note.h:46
shared_ptr< Pitch > mPitchDetector
Definition note.h:69
u8 frequencyToMidiNote(float hz) const
Definition note.cpp.hpp:158
u8 calculateVelocity(float energy, float confidence) const
Definition note.cpp.hpp:206
void update(shared_ptr< Context > context) override
Definition note.cpp.hpp:48
static constexpr float A4_FREQUENCY
Definition note.h:108
bool shouldTriggerNoteChange(u8 newNote, u8 currentNote) const
Definition note.cpp.hpp:237
static constexpr u8 NO_NOTE
Definition note.h:110
Pitch - Continuous pitch tracking using autocorrelation.
Definition pitch.h:33
unsigned char u8
Definition stdint.h:131
float powf(float base, float exponent) FL_NOEXCEPT
Definition math.h:436
shared_ptr< T > make_shared(Args &&... args) FL_NOEXCEPT
Definition shared_ptr.h:414
constexpr enable_if< is_fixed_point< T >::value, T >::type abs(T x) FL_NOEXCEPT
float logf(float value) FL_NOEXCEPT
Definition math.h:418
constexpr enable_if< is_fixed_point< T >::value, T >::type clamp(T x, T lo, T hi) FL_NOEXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT