diff --git a/src/musique.hh b/src/musique.hh index 3416a70..86976f0 100644 --- a/src/musique.hh +++ b/src/musique.hh @@ -447,6 +447,27 @@ struct Block Result index(Interpreter &i, unsigned position); }; +struct Note +{ + /// Base of a note, like `c` (=0), `c#` (=1) `d` (=2) + u8 base; + + /// Octave in MIDI acceptable range (from -1 to 9 inclusive) + std::optional octave = std::nullopt; + + /// Length of playing note + std::optional length = std::nullopt; + + /// Create Note from string + static std::optional from(std::string_view note); + + /// Extract midi note number + std::optional into_midi_note() const; + + /// Extract midi note number, but when octave is not present use provided default + u8 into_midi_note(i8 default_octave) const; +}; + template constexpr auto is_one_of = (std::is_same_v || ...); @@ -466,7 +487,8 @@ struct Value Number, Symbol, Intrinsic, - Block + Block, + Music }; Value() = default; @@ -480,10 +502,11 @@ struct Value } Type type = Type::Nil; - bool b{}; - Number n{}; - Intrinsic intr{}; + bool b; + Number n; + Intrinsic intr; Block blk; + Note note; // TODO Most strings should not be allocated by Value, but reference to string allocated previously // Wrapper for std::string is needed that will allocate only when needed, middle ground between: diff --git a/src/tests/value.cc b/src/tests/value.cc index 348fe6b..bd0df69 100644 --- a/src/tests/value.cc +++ b/src/tests/value.cc @@ -34,6 +34,23 @@ static void either_truthy_or_falsy( } } +static void test_note_resolution( + std::string_view name, + i8 octave, + u8 expected, + reflection::source_location sl = reflection::source_location::current()) +{ + auto const maybe_note = Note::from(name); + expect(maybe_note.has_value(), sl) << "Note::from didn't recognized " << name << " as a note"; + if (maybe_note) { + auto note = *maybe_note; + note.octave = octave; + auto const midi_note = note.into_midi_note(); + expect(midi_note.has_value(), sl) << "Note::into_midi_note returned nullopt, but should not"; + expect(eq(int(*midi_note), int(expected)), sl) << "Note::into_midi_note returned wrong value"; + } +} + suite value_test = [] { "Value"_test = [] { should("be properly created using Value::from") = [] { @@ -74,4 +91,24 @@ suite value_test = [] { expect(eq(""sv, str(Value(Intrinsic(nullptr))))); }; + + "Note"_test = [] { + should("properly resolve notes") = [] { + for (i8 i = 0; i < 9; ++i) { + test_note_resolution("c", i + -1, i * 12 + 0); + test_note_resolution("c#", i + -1, i * 12 + 1); + test_note_resolution("d", i + -1, i * 12 + 2); + test_note_resolution("d#", i + -1, i * 12 + 3); + test_note_resolution("e", i + -1, i * 12 + 4); + test_note_resolution("e#", i + -1, i * 12 + 5); + test_note_resolution("f", i + -1, i * 12 + 5); + test_note_resolution("f#", i + -1, i * 12 + 6); + test_note_resolution("g", i + -1, i * 12 + 7); + test_note_resolution("g#", i + -1, i * 12 + 8); + test_note_resolution("a", i + -1, i * 12 + 9); + test_note_resolution("a#", i + -1, i * 12 + 10); + test_note_resolution("b", i + -1, i * 12 + 11); + } + }; + }; }; diff --git a/src/value.cc b/src/value.cc index 279d945..d515490 100644 --- a/src/value.cc +++ b/src/value.cc @@ -11,6 +11,8 @@ constexpr u16 hash_note(Indexable auto const& note) return u8(note[0]) | (note[1] << 8); } +/// Finds numeric value of note. This form is later used as in +/// note to midi resolution in formula octave * 12 + note_index constexpr u8 note_index(Indexable auto const& note) { switch (hash_note(note)) { @@ -19,7 +21,7 @@ constexpr u8 note_index(Indexable auto const& note) case hash_note("d"): return 2; case hash_note("d#"): return 3; case hash_note("e"): return 4; - case hash_note("e#"): return 4; + case hash_note("e#"): return 5; case hash_note("f"): return 5; case hash_note("f#"): return 6; case hash_note("g"): return 7; @@ -32,7 +34,8 @@ constexpr u8 note_index(Indexable auto const& note) case hash_note("b#"): return 12; } // This should be unreachable since parser limits what character can pass as notes - unreachable(); + // but just to be sure return special value + return -1; } Result Value::from(Token t) @@ -113,6 +116,7 @@ bool Value::truthy() const case Type::Number: return n != Number(0); case Type::Block: case Type::Intrinsic: + case Type::Music: case Type::Symbol: return true; } unreachable(); @@ -135,6 +139,7 @@ bool Value::operator==(Value const& other) const case Type::Intrinsic: return intr == other.intr; case Type::Block: return false; // TODO Reconsider if functions are comparable case Type::Bool: return b == other.b; + case Type::Music: unimplemented(); } unreachable(); @@ -160,6 +165,9 @@ std::ostream& operator<<(std::ostream& os, Value const& v) case Value::Type::Block: return os << ""; + + case Value::Type::Music: + unimplemented(); } unreachable(); } @@ -167,9 +175,10 @@ std::ostream& operator<<(std::ostream& os, Value const& v) std::string_view type_name(Value::Type t) { switch (t) { + case Value::Type::Block: return "block"; case Value::Type::Bool: return "bool"; case Value::Type::Intrinsic: return "intrinsic"; - case Value::Type::Block: return "block"; + case Value::Type::Music: return "music"; case Value::Type::Nil: return "nil"; case Value::Type::Number: return "number"; case Value::Type::Symbol: return "symbol"; @@ -207,3 +216,23 @@ Result Block::index(Interpreter &i, unsigned position) assert(position < body.arguments.size(), "Out of range"); // TODO(assert) return i.eval((Ast)body.arguments[position]); } + +std::optional Note::from(std::string_view literal) +{ + if (auto note = note_index(literal); note != u8(-1)) { + return Note { .base = note }; + } + return std::nullopt; +} + +std::optional Note::into_midi_note() const +{ + return octave ? std::optional(into_midi_note(0)) : std::nullopt; +} + +u8 Note::into_midi_note(i8 default_octave) const +{ + auto const octave = this->octave.has_value() ? *this->octave : default_octave; + // octave is in range [-1, 9] where Note { .base = 0, .octave = -1 } is midi note 0 + return (octave + 1) * 12 + base; +}