Skip to content

Commit cb2fca7

Browse files
authored
Merge pull request #1159 from kushview/midi-monitor-display
fix: display correct note name in midi monitor and elsewhere
2 parents 54c1d41 + e6f0f82 commit cb2fca7

7 files changed

Lines changed: 127 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
## [1.2.x]
44

5+
### Changed
6+
- Note names throughout the UI now use scientific pitch notation (middle C = C4), matching the convention used by most DAWs.
7+
58
### Fixed
69
- Blocks embedded in the graph return to their embedded state when the plugin window is closed.
10+
- MIDI Monitor note names displayed two octaves too high.
11+
- MIDI Monitor logged Start/Stop/Continue messages twice.
712

813
## [1.2.0]
914

src/nodes/midimonitor.cpp

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: GPL-3.0-or-later
33

44
#include "nodes/midimonitor.hpp"
5+
#include "utils.hpp"
56

67
namespace element {
78

@@ -72,6 +73,32 @@ void MidiMonitorNode::clearMessages()
7273
messagesLogged();
7374
}
7475

76+
String MidiMonitorNode::describe (const MidiMessage& msg)
77+
{
78+
if (msg.isMidiClock())
79+
return {};
80+
81+
if (msg.isMidiStart())
82+
return "Start";
83+
if (msg.isMidiStop())
84+
return "Stop";
85+
if (msg.isMidiContinue())
86+
return "Continue";
87+
88+
if (msg.isNoteOn() || msg.isNoteOff())
89+
{
90+
String text;
91+
text << (msg.isNoteOn() ? "Note On " : "Note Off ")
92+
<< Util::noteValueToString (msg.getNoteNumber())
93+
<< " (" << msg.getNoteNumber() << ")"
94+
<< " Velocity " << (int) msg.getVelocity()
95+
<< " Channel " << msg.getChannel();
96+
return text;
97+
}
98+
99+
return msg.getDescription();
100+
}
101+
75102
void MidiMonitorNode::timerCallback()
76103
{
77104
midiTemp.clear();
@@ -80,48 +107,13 @@ void MidiMonitorNode::timerCallback()
80107
return;
81108

82109
int numLogged = 0;
83-
String text;
84110
for (auto m : midiTemp)
85111
{
86-
auto msg = m.getMessage();
87-
if (msg.isMidiClock())
88-
{
89-
text.clear();
112+
const auto text = describe (m.getMessage());
113+
if (text.isEmpty())
90114
continue;
91-
}
92-
93-
if (msg.isMidiStart())
94-
text << "Start";
95-
else if (msg.isMidiStop())
96-
text << "Stop";
97-
else if (msg.isMidiContinue())
98-
text << "Continue";
99-
100-
if (text.isNotEmpty())
101-
{
102-
midiLog.add (text);
103-
}
104-
else if (msg.isNoteOn())
105-
{
106-
text.clear();
107-
text << "Note On "
108-
<< msg.getMidiNoteName (msg.getNoteNumber(), true, true, 5)
109-
<< " (" << msg.getNoteNumber() << ") "
110-
<< " Velocity " << msg.getVelocity()
111-
<< " Channel " << msg.getChannel();
112-
}
113-
else if (msg.isNoteOff())
114-
{
115-
text.clear();
116-
text << "Note Off "
117-
<< msg.getMidiNoteName (msg.getNoteNumber(), true, true, 5)
118-
<< " (" << msg.getNoteNumber() << ") "
119-
<< " Velocity " << msg.getVelocity()
120-
<< " Channel " << msg.getChannel();
121-
}
122-
123-
midiLog.add (text.isNotEmpty() ? text : msg.getDescription());
124-
text.clear();
115+
116+
midiLog.add (text);
125117
++numLogged;
126118
}
127119

src/nodes/midimonitor.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ class MidiMonitorNode : public MidiFilterNode,
5858

5959
const StringArray& logger() const { return midiLog; }
6060

61+
/** Formats a MIDI message the way it appears in the monitor log.
62+
63+
@param msg the message to describe
64+
@return the display text, or an empty string for messages that are not
65+
logged (MIDI clock)
66+
*/
67+
static juce::String describe (const juce::MidiMessage& msg);
68+
6169
private:
6270
friend class MidiMonitorNodeEditor;
6371
friend class MidiMonitorBlock;

src/ui/virtualkeyboardview.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <element/audioengine.hpp>
55
#include "ui/guicommon.hpp"
66
#include "ui/virtualkeyboardview.hpp"
7+
#include "utils.hpp"
78

89
namespace element {
910

@@ -36,6 +37,7 @@ static int getOctaveOffsetForKeyPress (const KeyPress& key, const int fallback =
3637
VirtualKeyboardComponent::VirtualKeyboardComponent (MidiKeyboardState& s, Orientation o)
3738
: MidiKeyboardComponent (s, o)
3839
{
40+
setOctaveForMiddleC (Util::middleCOctave);
3941
}
4042

4143
void VirtualKeyboardComponent::setKeypressOctaveOffset (int offset)

src/utils.hpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,36 @@ inline static String secondsToString (const double input)
3434
return minutesToString (input / 60.0);
3535
}
3636

37+
/** The octave number given to middle C (MIDI note 60) everywhere note names are
38+
displayed or parsed.
39+
40+
Element uses scientific pitch notation, middle C = C4, which is what most DAWs
41+
show. Anything naming a MIDI note should go through noteValueToString() or use
42+
this constant rather than passing its own number to juce::MidiMessage.
43+
*/
44+
inline constexpr int middleCOctave = 4;
45+
46+
/** Converts a MIDI note number to its note name, e.g. "C4", "F#2", "A0".
47+
48+
@param value the MIDI note number, rounded to the nearest integer
49+
@return the note name including its octave number
50+
*/
3751
inline static String noteValueToString (double value)
3852
{
39-
return juce::MidiMessage::getMidiNoteName (juce::roundToInt (value), true, true, 3);
53+
return juce::MidiMessage::getMidiNoteName (juce::roundToInt (value), true, true, middleCOctave);
4054
}
4155

42-
/** Parses a note name (e.g. "C3", "F#2", "Eb4") or a raw MIDI number into a
56+
/** Parses a note name (e.g. "C4", "F#2", "Eb4") or a raw MIDI number into a
4357
MIDI note number 0-127.
4458
45-
Uses the same octave-for-middle-C convention as noteValueToString (middle C = C3),
59+
Uses the same octave-for-middle-C convention as noteValueToString (middle C = C4),
4660
so this is the inverse of that function.
4761
4862
@param text the text to parse
4963
@param octaveForMiddleC the octave number assigned to middle C (note 60)
5064
@return the MIDI note number 0-127, or -1 if the text cannot be parsed
5165
*/
52-
inline static int noteValueFromString (const juce::String& text, int octaveForMiddleC = 3)
66+
inline static int noteValueFromString (const juce::String& text, int octaveForMiddleC = middleCOctave)
5367
{
5468
const auto trimmed = text.trim();
5569
if (trimmed.isEmpty())

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ add_test(NAME "MidiMappingSessionTests" COMMAND test_element --run_test=MidiMapp
7777
add_test(NAME "MappingEngineTests" COMMAND test_element --run_test=MappingEngineTests)
7878
add_test(NAME "MappingLearnTests" COMMAND test_element --run_test=MappingLearnTests)
7979
add_test(NAME "MidiClockTest" COMMAND test_element --run_test=MidiClockTest)
80+
add_test(NAME "MidiMonitorTests" COMMAND test_element --run_test=MidiMonitorTests)
8081
add_test(NAME "MidiProgramMapTests" COMMAND test_element --run_test=MidiProgramMapTests)
8182
add_test(NAME "MidiProgramTests" COMMAND test_element --run_test=MidiProgramTests)
8283
add_test(NAME "MidiScriptTests" COMMAND test_element --run_test=MidiScriptTests)

test/MidiMonitorTests.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-FileCopyrightText: Copyright (C) Kushview, LLC.
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
#include <boost/test/unit_test.hpp>
5+
6+
#include "nodes/midimonitor.hpp"
7+
#include "utils.hpp"
8+
9+
using namespace element;
10+
using namespace juce;
11+
12+
BOOST_AUTO_TEST_SUITE (MidiMonitorTests)
13+
14+
BOOST_AUTO_TEST_CASE (NoteOnUsesMiddleCAsC4)
15+
{
16+
const auto text = MidiMonitorNode::describe (MidiMessage::noteOn (1, 60, 1.0f));
17+
BOOST_REQUIRE (text.startsWith ("Note On C4"));
18+
BOOST_REQUIRE (text.contains ("(60)"));
19+
BOOST_REQUIRE (text.contains ("Channel 1"));
20+
}
21+
22+
BOOST_AUTO_TEST_CASE (NoteNamesAtOctaveEdges)
23+
{
24+
BOOST_REQUIRE (MidiMonitorNode::describe (MidiMessage::noteOn (1, 21, 1.0f))
25+
.startsWith ("Note On A0"));
26+
BOOST_REQUIRE (MidiMonitorNode::describe (MidiMessage::noteOn (1, 0, 1.0f))
27+
.startsWith ("Note On C-1"));
28+
BOOST_REQUIRE (MidiMonitorNode::describe (MidiMessage::noteOn (1, 127, 1.0f))
29+
.startsWith ("Note On G9"));
30+
}
31+
32+
BOOST_AUTO_TEST_CASE (NoteOff)
33+
{
34+
const auto text = MidiMonitorNode::describe (MidiMessage::noteOff (2, 60));
35+
BOOST_REQUIRE (text.startsWith ("Note Off C4"));
36+
BOOST_REQUIRE (text.contains ("Channel 2"));
37+
}
38+
39+
BOOST_AUTO_TEST_CASE (ClockIsNotLogged)
40+
{
41+
BOOST_REQUIRE (MidiMonitorNode::describe (MidiMessage::midiClock()).isEmpty());
42+
}
43+
44+
BOOST_AUTO_TEST_CASE (TransportMessagesLogOnce)
45+
{
46+
BOOST_REQUIRE_EQUAL (MidiMonitorNode::describe (MidiMessage::midiStart()).toStdString(), "Start");
47+
BOOST_REQUIRE_EQUAL (MidiMonitorNode::describe (MidiMessage::midiStop()).toStdString(), "Stop");
48+
BOOST_REQUIRE_EQUAL (MidiMonitorNode::describe (MidiMessage::midiContinue()).toStdString(), "Continue");
49+
}
50+
51+
BOOST_AUTO_TEST_CASE (AgreesWithNoteValueConversions)
52+
{
53+
BOOST_REQUIRE_EQUAL (Util::middleCOctave, 4);
54+
BOOST_REQUIRE_EQUAL (Util::noteValueToString (60).toStdString(), "C4");
55+
BOOST_REQUIRE_EQUAL (Util::noteValueToString (21).toStdString(), "A0");
56+
BOOST_REQUIRE_EQUAL (Util::noteValueFromString ("C4"), 60);
57+
BOOST_REQUIRE_EQUAL (Util::noteValueFromString ("A0"), 21);
58+
BOOST_REQUIRE_EQUAL (Util::noteValueFromString ("C-1"), 0);
59+
BOOST_REQUIRE_EQUAL (Util::noteValueFromString ("G9"), 127);
60+
}
61+
62+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)