Skip to content

Commit

Permalink
Changed default middle C octave to 3 for note names, which is the mos…
Browse files Browse the repository at this point in the history
…t common one.

Added support for virtual MIDI output ports on Linux and macOS.
Added support for relative timestamps.
  • Loading branch information
gbevin committed Feb 28, 2017
1 parent 062ea49 commit 277961a
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 45 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ To use it, simply type "sendmidi" or "sendmidi.exe" on the command line and foll

These are all the supported commands:
```
dev name Set the name of the MIDI output port (REQUIRED)
dev name Set the name of the MIDI output port
virt (name) Use virtual MIDI port with optional name (Linux/macOS)
list Lists the MIDI output ports
panic Sends all possible Note Offs and relevant panic CCs
file path Loads commands from the specified program file
dec Interpret the next numbers as decimals by default
hex Interpret the next numbers as hexadecimals by default
ch number Set MIDI channel for the commands (1-16), defaults to 1
omc number Set octave for middle C, defaults to 5
omc number Set octave for middle C, defaults to 3
on note velocity Send Note On with note (0-127) and velocity (0-127)
off note velocity Send Note Off with note (0-127) and velocity (0-127)
pp note value Send Poly Pressure with note (0-127) and value (0-127)
Expand Down Expand Up @@ -67,7 +68,7 @@ These are all the supported commands:

Alternatively, you can use the following long versions of the commands:
```
device decimal hexadecimal channel octave-middle-c note-on note-off
device virtual decimal hexadecimal channel octave-middle-c note-on note-off
poly-pressure control-change program-change channel-pressure pitch-bend
midi-clock continue active-sensing reset system-exclusive
system-exclusive-file time-code song-position song-select tune-request
Expand All @@ -80,10 +81,12 @@ Additionally, by suffixing a number with "M" or "H", it will be interpreted as a
The MIDI device name doesn't have to be an exact match.
If SendMIDI can't find the exact name that was specified, it will pick the first MIDI output port that contains the provided text, irrespective of case.

Where notes can be provided as arguments, they can also be written as note names, by default from C0 to G10 which corresponds to note numbers 0 to 127. By setting the octave for middle C, the note name range can be changed. Sharps can be added by using the '#' symbol after the note letter, and flats by using the letter 'b'.
Where notes can be provided as arguments, they can also be written as note names, by default from C-2 to G8 which corresponds to note numbers 0 to 127. By setting the octave for middle C, the note name range can be changed. Sharps can be added by using the '#' symbol after the note letter, and flats by using the letter 'b'.

In between commands, timestamps can be added in the format: HH:MM:SS.MIL, standing for hours, minutes, seconds and milliseconds (for example: 08:10:17.056). All the digits need to be present, possibly requiring leading zeros. When a timestamp is detected, SendMIDI ensures that the time difference since the previous timestamp has elapsed.

When a timestamp is prefixed with a plus sign, it's considered relative and will be processed as a time offset instead of an absolute time. For example +00:00:01.060 will execute the next command one second and 60 milliseconds later. For convenience, a relative timestamp can also be shortened to +SS.MIL (for example: +01.060).

## Examples

Here are a few examples to get you started:
Expand Down
144 changes: 103 additions & 41 deletions Source/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum CommandIndex
LIST,
PANIC,
DEVICE,
VIRTUAL,
TXTFILE,
DECIMAL,
HEXADECIMAL,
Expand Down Expand Up @@ -56,7 +57,8 @@ enum CommandIndex
RAW_MIDI
};

static const int DEFAULT_OCTAVE_MIDDLE_C = 5;
static const int DEFAULT_OCTAVE_MIDDLE_C = 3;
static const String& DEFAULT_VIRTUAL_NAME = "SendMIDI";

struct ApplicationCommand
{
Expand Down Expand Up @@ -94,14 +96,15 @@ class sendMidiApplication : public JUCEApplicationBase
public:
sendMidiApplication()
{
commands_.add({"dev", "device", DEVICE, 1, "name", "Set the name of the MIDI output port (REQUIRED)"});
commands_.add({"dev", "device", DEVICE, 1, "name", "Set the name of the MIDI output port"});
commands_.add({"virt", "virtual", VIRTUAL, -1, "(name)", "Use virtual MIDI port with optional name (Linux/macOS)"});
commands_.add({"list", "", LIST, 0, "", "Lists the MIDI output ports"});
commands_.add({"panic", "", PANIC, 0, "", "Sends all possible Note Offs and relevant panic CCs"});
commands_.add({"file", "", TXTFILE, 1, "path", "Loads commands from the specified program file"});
commands_.add({"dec", "decimal", DECIMAL, 0, "", "Interpret the next numbers as decimals by default"});
commands_.add({"hex", "hexadecimal", HEXADECIMAL, 0, "", "Interpret the next numbers as hexadecimals by default"});
commands_.add({"ch", "channel", CHANNEL, 1, "number", "Set MIDI channel for the commands (1-16), defaults to 1"});
commands_.add({"omc", "octave-middle-c", OCTAVE_MIDDLE_C, 1, "number", "Set octave for middle C, defaults to 5"});
commands_.add({"omc", "octave-middle-c", OCTAVE_MIDDLE_C, 1, "number", "Set octave for middle C, defaults to 3"});
commands_.add({"on", "note-on", NOTE_ON, 2, "note velocity", "Send Note On with note (0-127) and velocity (0-127)"});
commands_.add({"off", "note-off", NOTE_OFF, 2, "note velocity", "Send Note Off with note (0-127) and velocity (0-127)"});
commands_.add({"pp", "poly-pressure", POLY_PRESSURE, 2, "note value", "Send Poly Pressure with note (0-127) and value (0-127)"});
Expand Down Expand Up @@ -203,15 +206,64 @@ class sendMidiApplication : public JUCEApplicationBase
{
return string.containsOnly("1234567890");
}

int64_t parseTimestamp(const String& param)
{
int64_t timestamp = 0;
if (param.length() == 12 && param[2] == ':' && param[5] == ':' && param[8] == '.')
{
String hours = param.substring(0, 2);
String minutes = param.substring(3, 5);
String seconds = param.substring(6, 8);
String millis = param.substring(9);
if (isNumeric(hours) && isNumeric(minutes) && isNumeric(seconds) && isNumeric(millis))
{
Time now = Time();
timestamp = Time(now.getYear(), now.getMonth(), now.getDayOfMonth(),
hours.getIntValue(), minutes.getIntValue(), seconds.getIntValue(), millis.getIntValue()).toMilliseconds();
}
}
else if (param.length() == 13 && param[0] == '+' && param[3] == ':' && param[6] == ':' && param[9] == '.')
{
String hours = param.substring(1, 3);
String minutes = param.substring(4, 6);
String seconds = param.substring(7, 9);
String millis = param.substring(10);
if (isNumeric(hours) && isNumeric(minutes) && isNumeric(seconds) && isNumeric(millis))
{
timestamp = (((int64_t(hours.getIntValue()) * 60 + int64_t(minutes.getIntValue())) * 60) + int64_t(seconds.getIntValue())) * 1000 + millis.getIntValue();
}
}
else if (param.length() == 7 && param[0] == '+' && param[3] == '.')
{
String seconds = param.substring(1, 3);
String millis = param.substring(4);
if (isNumeric(seconds) && isNumeric(millis))
{
timestamp = (int64_t(seconds.getIntValue())) * 1000 + millis.getIntValue();
}
}
return timestamp;
}

void handleVarArgCommand()
{
if (currentCommand_.expectedOptions_ < 0)
{
executeCommand(currentCommand_);
}
}

void parseParameters(StringArray& parameters)
{
for (String param : parameters)
{
if (param == "--") continue;

ApplicationCommand* cmd = findApplicationCommand(param);
if (cmd)
{
// handle configuration commands immediately without setting up a new
// handle configuration commands immediately without setting up a new one
switch (cmd->command_)
{
case DECIMAL:
Expand All @@ -221,38 +273,24 @@ class sendMidiApplication : public JUCEApplicationBase
useHexadecimalsByDefault_ = true;
break;
default:
// handle variable arg commands
if (currentCommand_.expectedOptions_ < 0)
{
executeCommand(currentCommand_);
}
handleVarArgCommand();

currentCommand_ = *cmd;
break;
}
}
else if (currentCommand_.command_ == NONE)
else
{
// check if this is a time stamp
int64_t timestamp = 0;
if (param.length() == 12 && param[2] == ':' && param[5] == ':' && param[8] == '.')
int64_t timestamp = parseTimestamp(param);
if (timestamp)
{
String hours = param.substring(0, 2);
String minutes = param.substring(3, 5);
String seconds = param.substring(6, 8);
String millis = param.substring(9);
if (isNumeric(hours) && isNumeric(minutes) && isNumeric(seconds) && isNumeric(millis))
handleVarArgCommand();

if (param[0] == '+')
{
Time now = Time();
timestamp = Time(now.getYear(), now.getMonth(), now.getDayOfMonth(),
hours.getIntValue(), minutes.getIntValue(), seconds.getIntValue(), millis.getIntValue()).toMilliseconds();
Time::waitForMillisecondCounter(uint32(Time::getMillisecondCounter() + timestamp));
}
}

// handle the timestamp
if (timestamp)
{
if (lastTimeStamp_ != 0)
else if (lastTimeStamp_ != 0)
{
// wait for the time that needs to have elapsed since the previous timestamp
uint32 now_counter = Time::getMillisecondCounter();
Expand All @@ -274,20 +312,19 @@ class sendMidiApplication : public JUCEApplicationBase
lastTimeStampCounter_ = Time::getMillisecondCounter();
lastTimeStamp_ = timestamp;
}
// treat it as a file
else
else if (currentCommand_.command_ == NONE)
{
File file = File::getCurrentWorkingDirectory().getChildFile(param);
if (file.existsAsFile())
{
parseFile(file);
}
}
}
else if (currentCommand_.expectedOptions_ != 0)
{
currentCommand_.opts_.add(param);
currentCommand_.expectedOptions_ -= 1;
else if (currentCommand_.expectedOptions_ != 0)
{
currentCommand_.opts_.add(param);
currentCommand_.expectedOptions_ -= 1;
}
}

// handle fixed arg commands
Expand All @@ -297,11 +334,7 @@ class sendMidiApplication : public JUCEApplicationBase
}
}

// handle variable arg commands
if (currentCommand_.expectedOptions_ < 0)
{
executeCommand(currentCommand_);
}
handleVarArgCommand();
}

void parseFile(File file)
Expand Down Expand Up @@ -375,6 +408,28 @@ class sendMidiApplication : public JUCEApplicationBase
}
break;
}
case VIRTUAL:
{
#if (JUCE_LINUX || JUCE_MAC)
String name = DEFAULT_VIRTUAL_NAME;
if (cmd.opts_.size())
{
name = cmd.opts_[0];
}
midiOut_ = MidiOutput::createNewDevice(name);
if (midiOut_ == nullptr)
{
std::cerr << "Couldn't create virtual MIDI output port \"" << name << "\"" << std::endl;
}
else
{
midiOutName_ = cmd.opts_[0];
}
#else
std::cerr << "Virtual MIDI output ports are not supported on Windows" << std::endl;
#endif
break;
}
case PANIC:
{
for (int ch = 1; ch <= 16; ++ch)
Expand Down Expand Up @@ -618,7 +673,7 @@ class sendMidiApplication : public JUCEApplicationBase
note += 1;
}

note += (value.getTrailingIntValue() + DEFAULT_OCTAVE_MIDDLE_C - octaveMiddleC_) * 12;
note += (value.getTrailingIntValue() + 5 - octaveMiddleC_) * 12;

return (uint8)limit7Bit(note);
}
Expand Down Expand Up @@ -714,7 +769,7 @@ class sendMidiApplication : public JUCEApplicationBase
<< "first MIDI output port that contains the provided text, irrespective of case." << std::endl;
std::cout << std::endl;
std::cout << "Where notes can be provided as arguments, they can also be written as note" << std::endl
<< "names, by default from C0 to G10 which corresponds to note numbers 0 to 127." << std::endl
<< "names, by default from C-2 to G8 which corresponds to note numbers 0 to 127." << std::endl
<< "By setting the octave for middle C, the note name range can be changed. " << std::endl
<< "Sharps can be added by using the '#' symbol after the note letter, and flats" << std::endl
<< "by using the letter 'b'. " << std::endl;
Expand All @@ -725,6 +780,13 @@ class sendMidiApplication : public JUCEApplicationBase
<< "requiring leading zeros. When a timestamp is detected, SendMIDI ensures that" << std::endl
<< "the time difference since the previous timestamp has elapsed." << std::endl;
std::cout << std::endl;
std::cout << "When a timestamp is prefixed with a plus sign, it's considered relative and" << std::endl
<< "will be processed as a time offset instead of an absolute time. For example" << std::endl
<< "+00:00:01.060 will execute the next command one second and 60 milliseconds" << std::endl
<< "later. For convenience, a relative timestamp can also be shortened to +SS.MIL" << std::endl
<< "(for example: +01.060)." << std::endl;

std::cout << std::endl;
}

Array<ApplicationCommand> commands_;
Expand Down

0 comments on commit 277961a

Please sign in to comment.