This commit is contained in:
zawz 2019-06-15 21:07:32 +02:00
parent abfb03189a
commit 40d0fcff6f
16 changed files with 732 additions and 0 deletions

87
README Normal file
View file

@ -0,0 +1,87 @@
Maps midi signals coming from ALSA midi devices to shell commands
For a release build: `$ make clean ; make -B RELEASE=true`
usage: midiMap <config file>
This is a daemon program, it does not start any background process by itself and needs to be constantly running for the mapping to be active
This program is in early stage but is fully functional without any major errors
TODO:
- Use integrated C MIDI control
- Support for multiple identical devices
- Support for system reserved commands
- Options
- Better error handling on wrong config file format
See 'example.mim' for an example config file
-- COMMAND FORMAT --
Format is a regular shell format
-- Environment
- Global
$channel: channel of the
$id: id of the note/controller
- Note
$velocity: velocity of the note
- Controller
$value: value of the controller (remapped)
$rawvalue: original value of the controller
-- FILE FORMAT --
[<device>,<device>]
- <device> format:
{
name=<name>
commands=[<command>,<command>]
}
-
*name: string referring to client name of the device ($ aseqdump -l)
- command format (global):
{
type=<note/controller>
id=<x>
shell=<shell command>
channel=<*/x>
}
-
*id: value from 0 to 127 referring to id of note/controller
> mandatory
*shell: shell command to be executed
> mandatory
*channel: value from 0 to 16 for channel. Can use * for any channel
> optional, default *
- <command> format (note)
{
trigger=<x:y/x>
}
-
*trigger: note velocity from 0 to 127 that triggers the command. Can enter an interval x:y or single value
> optional, default 1:127
- <command> format (note)
{
range=<x:y>
remap=<x:y>
float=<true/false>
}
-
*range: controller value from 0 to 127 that triggers command. Can enter an interval x:y or single value
> optional, default 0:127
*remap: remaps the range to given interval. Interval can be inversed and float
> optional, default same as range
*float: boolean value defining if output is a floating point value
> optional, default false
- Comments can be written inside {} by doing //=<COMMENT>

68
example.mim Normal file
View file

@ -0,0 +1,68 @@
[
{
name=Launch Control
commands=[
{
type=note
id=9
channel=0
trigger=1:127
shell=echo "Note $id on ch$channel v$velocity"
},
{
type=note
id=9
channel=0
trigger=0
shell=echo "Note $id off ch$channel"
},
{
//=KNOB L1
//=displays value 0:127 for knob 41 from any channel
type=controller
id=41
channel=*
shell=echo "Knob #$id ch$channel:$value"
},
{
//=KNOB L2
//=displays value -100:100 for knob 42 on channel 0
type=controller
id=42
channel=0
remap=-100:100
float=true
shell=echo "Knob #$id ch$channel:$value r:$rawvalue"
},
{
//=KNOB L3 H1
//=displays value 0:1:0 for knob 42 on channel 1 (first half)
type=controller
id=42
channel=1
range=0:63
remap=0:1
float=true
shell=echo "Knob #$id ch$channel:$value"
},
{
//=KNOB L3 H2
//=displays value 0:1:0 for knob 42 on channel 1 (second half)
type=controller
id=42
channel=1
range=64:127
remap=1:0
float=true
shell=echo "Knob #$id ch$channel:$value"
}
]
}
,
{
name=Launchpad S
commands=[]
}
]

1
include/Filedat.hpp Symbolic link
View file

@ -0,0 +1 @@
/home/zawz/code/c/tool/zFiledat/Filedat.hpp

38
include/command.hpp Normal file
View file

@ -0,0 +1,38 @@
#ifndef COMMAND_HPP
#define COMMAND_HPP
#include <stdlib.h>
#include <string>
class NoteCommand
{
public:
NoteCommand(uint8_t i, uint8_t ch, uint8_t l, uint8_t h, std::string sh);
uint8_t id;
uint8_t channel;
uint8_t low;
uint8_t high;
std::string shell;
};
class ControllerCommand
{
public:
ControllerCommand(uint8_t i, int8_t ch, uint8_t l, uint8_t h, float ml, float mm, bool fl, std::string sh);
uint8_t id;
int8_t channel;
uint8_t min;
uint8_t max;
float mapMin;
float mapMax;
bool floating;
std::string shell;
};
#endif //COMMAND_HPP

38
include/device.hpp Normal file
View file

@ -0,0 +1,38 @@
#ifndef DEVICE_HPP
#define DEVICE_HPP
#include <string>
#include <vector>
#include <thread>
#include "command.hpp"
#include "Filedat.hpp"
class Device
{
public:
Device();
virtual ~Device();
bool start_loop();
bool import_chunk(Chunk const& ch);
Chunk export_chunk();
std::string name;
bool busy;
uint32_t nb_command;
std::vector<NoteCommand> noteCommands[128];
std::vector<ControllerCommand> ctrlCommands[128];
// std::vector<Command> systCommands;
std::thread thread;
private:
static void loop(Device* dev);
};
extern std::vector<Device*> device_list;
#endif //DEVICE_HPP

1
include/options.hpp Symbolic link
View file

@ -0,0 +1 @@
/home/zawz/code/c/tool/zOptions/options.hpp

1
include/stringTools.hpp Symbolic link
View file

@ -0,0 +1 @@
/home/zawz/code/c/tool/stringTools/stringTools.hpp

8
include/system.hpp Normal file
View file

@ -0,0 +1,8 @@
#ifndef SYSTEM_HPP
#define SYSTEM_HPP
void device_check();
void announce_loop();
#endif //SYSTEM_HPP

36
makefile Normal file
View file

@ -0,0 +1,36 @@
IDIR=include
SRCDIR=src
ODIR=obj
BINDIR=bin
NAME = zmidimap
CC=g++
CXXFLAGS= -I$(IDIR) -Wall -pedantic -std=c++17
ifeq ($(RELEASE),true)
BINDIR=.
else
CXXFLAGS += -g
endif
LDFLAGS = -lpthread
# automatically finds .hpp
DEPS = $(shell if [ -n "$(ld $(IDIR))" ] ; then ls $(IDIR)/*.hpp ; fi)
# automatically finds .cpp and makes the corresponding .o rule
OBJ = $(shell ls $(SRCDIR)/*.cpp | sed 's/.cpp/.o/g;s|$(SRCDIR)/|$(ODIR)/|g')
$(ODIR)/%.o: $(SRCDIR)/%.cpp $(DEPS)
$(CC) $(CXXFLAGS) -c -o $@ $<
$(BINDIR)/$(NAME): $(OBJ) $(DEPS)
$(CC) $(CXXFLAGS) -o $@ $^ $(LDFLAGS)
test: $(BINDIR)/$(NAME)
$(BINDIR)/$(NAME)
clean:
rm $(ODIR)/*.o
clear:
rm $(BINDIR)/$(NAME)

1
src/Filedat.cpp Symbolic link
View file

@ -0,0 +1 @@
/home/zawz/code/c/tool/zFiledat/Filedat.cpp

22
src/command.cpp Normal file
View file

@ -0,0 +1,22 @@
#include "command.hpp"
NoteCommand::NoteCommand(uint8_t i, uint8_t ch, uint8_t l, uint8_t h, std::string sh)
{
this->id=i;
this->channel=ch;
this->low=l;
this->high=h;
this->shell=sh;
}
ControllerCommand::ControllerCommand(uint8_t i, int8_t ch, uint8_t l, uint8_t h, float ml, float mh, bool fl, std::string sh)
{
this->id=i;
this->channel=ch;
this->min=l;
this->max=h;
this->mapMin=ml;
this->mapMax=mh;
this->floating=fl;
this->shell=sh;
}

289
src/device.cpp Normal file
View file

@ -0,0 +1,289 @@
#include "device.hpp"
#include "stringTools.hpp"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <iostream>
#define KILL_COMMAND_FH "kill -s INT $(ps -f | grep \"aseqdump -p "
#define KILL_COMMAND_SH "\" | grep -v grep | awk '{print $2}' | head -n1)"
std::vector<Device*> device_list;
static void sh(std::string const& string)
{
system(string.c_str());
}
Device::Device()
{
busy=false;
nb_command=0;
}
Device::~Device()
{
}
bool Device::start_loop()
{
if(this->busy)
return false;
this->busy = true;
this->thread = std::thread(Device::loop, this);
this->thread.detach();
return true;
}
bool Device::import_chunk(Chunk const& ch)
{
Chunk& cch = ch["commands"];
this->name=ch["name"].strval();
for(int i=0 ; i<cch.listSize() ; i++)
{
Chunk& tch=cch[i];
std::string tstr=tch["type"].strval();
if(tstr == "system") //type system
{
throw std::runtime_error("System commands not implemented yet");
}
else
{
int8_t channel;
uint8_t id;
std::string shell;
//id
id=stoi(tch["id"].strval());
//channel
if(tch["channel"].strval()=="*")
channel=-1;
else
channel=stoi(tch["channel"].strval());
//shell
shell=tch["shell"].strval();
//type
if(tstr == "note") //type note
{
uint8_t low;
uint8_t high;
std::string tt;
//trigger
tt=tch["trigger"].strval();
auto tpos=tt.find(':');
if(tpos == std::string::npos)
{
//single value
low=stoi(tt);
high=low;
}
else
{
//range
low=stoi(tt.substr(0,tpos));
tpos++;
high=stoi(tt.substr(tpos, tt.size()-tpos));
}
this->noteCommands[id].push_back(NoteCommand(id,channel,low,high,shell));
this->nb_command++;
}
else if(tstr == "controller") //type controller
{
uint8_t min=0;
uint8_t max=127;
float mapMin;
float mapMax;
bool floating=false;
//range
Chunk* ttch=tch.subChunkPtr("range");
if(ttch != nullptr)
{
std::string range=ttch->strval();
auto tpos=range.find(':');
if(tpos == std::string::npos)
{
//single value
min=stoi(range);
max=min;
}
else
{
//range
min=stoi(range.substr(0,tpos));
tpos++;
max=stoi(range.substr(tpos, range.size()-tpos));
}
}
//remap
ttch=tch.subChunkPtr("remap");
if(ttch != nullptr)
{
std::string map=ttch->strval();
auto tpos=map.find(':');
mapMin=stof(map.substr(0,tpos));
tpos++;
mapMax=stof(map.substr(tpos, map.size()-tpos));
}
//floating
ttch=tch.subChunkPtr("float");
if(ttch != nullptr)
{
std::string tfl=ttch->strval();
if( tfl == "true" || tfl == "yes" || tfl == "y")
floating=true;
}
else
{
mapMin=min;
mapMax=max;
}
this->ctrlCommands[id].push_back(ControllerCommand(id,channel,min,max,mapMin,mapMax,floating,shell));
this->nb_command++;
}
else
{
throw std::runtime_error("Command type " + tstr + " doesn't exist");
return false;
}
}
}
return true;
}
void Device::loop(Device* dev)
{
std::string command = "aseqdump -p '" + dev->name + '\'';
FILE *stream = popen(command.c_str(), "r");
char* buff = NULL;
size_t buff_size = 0;
while (getline(&buff, &buff_size, stream) > 0)
{
if ( (strstr(buff, "Port unsubscribed") != NULL) ) // distonnected
{
std::string kill_command=KILL_COMMAND_FH + dev->name + KILL_COMMAND_SH;
system(kill_command.c_str()); // kill the process
dev->busy=false;
}
else if (index(buff, ':') != NULL) // MIDI command
{
if (strstr(buff, "System exclusive") != NULL)
{
printf("%s", buff);
//do stuff
}
else
{
int t;
char type;
int8_t channel;
int ctid;
uint8_t value;
char* pos=NULL;
bool note_off=false;
if (strstr(buff,"Note off") != NULL)
note_off=true;
//channel read
pos=index(buff, ',');
t=1;
while (*(pos-t-1) != ' ')
t++;
channel = std::stoi( std::string(pos-t, t) );
pos+=2;
//type
if (*pos == 'c') //controller signal
type='c';
else if (*pos == 'n') //note signal
type='n';
else
{
throw std::runtime_error("Unknown MIDI signal\n");
return;
}
//ctid read
while (*(pos) != ' ')
pos++;
pos++;
t=1;
while ( isNum(*(pos+t)) )
t++;
ctid = std::stoi( std::string(pos, t) );
pos+=t+2;
//value read
if(!note_off)
{
while (*(pos) != ' ')
pos++;
pos++;
t=1;
while ( isNum(*(pos+t)) )
t++;
value = std::stoi( std::string(pos, t));
}
else
value=0;
if (type == 'n')
{
for( auto it : dev->noteCommands[ctid])
{
if((it.channel == -1 || it.channel == channel) && it.low <= value && it.high >= value)
{
std::string command="id=" + std::to_string(ctid) + ";channel=" + std::to_string(channel) + ";velocity=" + std::to_string(value) + ";";
command += it.shell;
std::thread(sh, command).detach();
}
}
}
if(type == 'c')
{
for( auto it : dev->ctrlCommands[ctid])
{
if((it.channel == -1 || it.channel == channel) && it.min <= value && it.max >= value)
{
//remapping of value
float result;
if(it.min == it.max)
result = it.mapMin;
else
result=(value-it.min)*(it.mapMax-it.mapMin)/(it.max-it.min)+it.mapMin;
//command execution
std::string command="id=" + std::to_string(ctid) + ";channel=" + std::to_string(channel) + ";rawvalue=" + std::to_string(value) + ";value=";
if(it.floating)
command += std::to_string(result);
else
command += std::to_string((long int) result);
command += ";" + it.shell;
std::thread(sh, command).detach();
}
}
}
}
}
}
printf("Device '%s' disconnected\n", dev->name.c_str());
pclose(stream);
free(buff);
}

79
src/main.cpp Normal file
View file

@ -0,0 +1,79 @@
#include <signal.h>
#include <stdio.h>
#include "device.hpp"
#include "system.hpp"
#include "Filedat.hpp"
#include "options.hpp"
int main(int argc, char* argv[])
{
signal(SIGCHLD, SIG_IGN); //signal that we aren't expecting returns from child processes
OptionSet options;
options.addOption(Option('f',"file",true));
auto argvec = argVector(argc, argv);
auto t = options.getOptions(argvec);
std::vector<std::string> arg=t.first;
if( !t.second )
{
fprintf(stderr, "Unexpected error\n");
return 1;
}
if (arg.size() <= 0 || arg[0] == "")
{
fprintf(stderr, "No config file specified\n");
return 2;
}
Filedat file(arg[0]);
if (!file.readTest())
{
fprintf(stderr, "File '%s' unavailable\n", arg[0].c_str());
return 10;
}
printf("Loading config file '%s'\n", arg[0].c_str());
bool import_ok;
try
{
import_ok = file.importFile();
if(import_ok)
{
for(int i=0 ; i<file.chunk().listSize() ; i++)
{
Device *newDevice = new Device;
newDevice->import_chunk(file.chunk()[i]);
device_list.push_back(newDevice);
printf("Added device '%s' with %d commands\n", newDevice->name.c_str(), newDevice->nb_command);
}
}
else
{
fprintf(stderr, "Unknown config file error\n");
return 2;
}
printf("Starting scan for devices\n");
announce_loop();
for(auto it : device_list)
{
delete it;
}
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << std::endl;
return 11;
}
return 0;
}

1
src/options.cpp Symbolic link
View file

@ -0,0 +1 @@
/home/zawz/code/c/tool/zOptions/options.cpp

1
src/stringTools.cpp Symbolic link
View file

@ -0,0 +1 @@
/home/zawz/code/c/tool/stringTools/stringTools.cpp

61
src/system.cpp Normal file
View file

@ -0,0 +1,61 @@
#include "system.hpp"
#include "device.hpp"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <vector>
#define ANNOUNCE_COMMAND "aseqdump -p System:1"
#define LIST_COMMAND "aseqdump -l | tail -n +2 | tr -s ' ' | cut -d' ' -f3-"
void device_check()
{
char* buff = NULL;
size_t buff_size = 0;
FILE *stream = popen(LIST_COMMAND, "r");
std::string str;
getline(&buff, &buff_size, stream); //discard the first line
while ( getline(&buff, &buff_size, stream) > 0 ) //retrieve device lines
{
str += buff;
}
for ( auto it : device_list ) // iterate devices
{
if( !it->busy && str.find(it->name) != std::string::npos ) //device detected
{
printf("Device '%s' found\n", it->name.c_str());
it->start_loop();
}
}
if(buff != NULL)
free(buff);
}
void announce_loop()
{
char* buff = NULL;
size_t buff_size = 0;
FILE *stream = popen(ANNOUNCE_COMMAND,"r");
if (stream == NULL)
{
fprintf(stderr, "Error\n");
return;
}
while ( getline(&buff, &buff_size, stream) > 0 )
{
if ( (strstr(buff, "Port start") != NULL) || (strstr(buff,"Port subscribed") != NULL) )
device_check();
}
if(buff != NULL)
free(buff);
}