// note_entry.cpp — see note_entry.h. PURE text->MIDI-note parse for the S12 numeric entry. #include "note_entry.h" #include #include namespace reasampler::vst { namespace { char asciiUpper(char c) { return static_cast(std::toupper(static_cast(c))); } std::string trim(const std::string& s) { std::size_t a = 0; std::size_t b = s.size(); while (a < b && std::isspace(static_cast(s[a]))) ++a; while (b > a && std::isspace(static_cast(s[b - 1]))) --b; return s.substr(a, b - a); } int clampNote(long long n) { if (n < 0) return 0; if (n > 127) return 127; return static_cast(n); } // Semitone offset within an octave for a note letter (C..B), or -1 for a non-letter. int letterSemitone(char up) { switch (up) { case 'C': return 0; case 'D': return 2; case 'E': return 4; case 'F': return 5; case 'G': return 7; case 'A': return 9; case 'B': return 11; default: return -1; } } // Parse a note name like "C4", "F#3", "Bb-1" (case-insensitive). MIDI 0 == C-1, 60 == C4 // (the DAW convention the editor's noteLabel uses). Returns nullopt if it is not a note name. std::optional parseNoteName(const std::string& s) { if (s.empty()) return std::nullopt; std::size_t i = 0; const int base = letterSemitone(asciiUpper(s[i])); if (base < 0) return std::nullopt; // not a letter -> not a note name ++i; int semitone = base; // Optional accidental(s): # / b (or 's'/'f' are NOT accepted — keep it to the two glyphs). while (i < s.size() && (s[i] == '#' || s[i] == 'b' || s[i] == 'B')) { // A trailing 'b'/'B' could be a flat OR the start of nothing; here after a letter it is // an accidental. '#' raises, 'b'/'B' lowers. if (s[i] == '#') ++semitone; else --semitone; ++i; } // The octave: an optional sign then digits, running to the end. if (i >= s.size()) return std::nullopt; // a bare "C" has no octave -> reject (ambiguous) bool neg = false; if (s[i] == '+' || s[i] == '-') { neg = (s[i] == '-'); ++i; } if (i >= s.size()) return std::nullopt; int octave = 0; bool anyDigit = false; for (; i < s.size(); ++i) { if (!std::isdigit(static_cast(s[i]))) return std::nullopt; octave = octave * 10 + (s[i] - '0'); anyDigit = true; } if (!anyDigit) return std::nullopt; if (neg) octave = -octave; // MIDI note = (octave + 1) * 12 + semitone (C-1 == 0, C4 == 60). const long long note = static_cast(octave + 1) * 12 + semitone; return clampNote(note); } std::optional parseInteger(const std::string& s) { if (s.empty()) return std::nullopt; std::size_t i = 0; bool neg = false; if (s[i] == '+' || s[i] == '-') { neg = (s[i] == '-'); ++i; } if (i >= s.size()) return std::nullopt; long long v = 0; for (; i < s.size(); ++i) { if (!std::isdigit(static_cast(s[i]))) return std::nullopt; v = v * 10 + (s[i] - '0'); if (v > 1000000) v = 1000000; // saturate; clampNote takes it to 127 anyway } if (neg) v = -v; return clampNote(v); } } // namespace std::optional parseNoteEntry(const std::string& text) { const std::string s = trim(text); if (s.empty()) return std::nullopt; // Try a plain integer first (the common MIDI-number case); fall back to a note name. if (std::isdigit(static_cast(s[0])) || s[0] == '+' || (s[0] == '-' && s.size() > 1 && std::isdigit(static_cast(s[1])))) { if (auto n = parseInteger(s)) return n; } return parseNoteName(s); } } // namespace reasampler::vst