diff --git a/README b/README new file mode 100644 index 0000000..428def9 --- /dev/null +++ b/README @@ -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 +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 -- +[,] + +- format: +{ +name= +commands=[,] +} +- +*name: string referring to client name of the device ($ aseqdump -l) + +- command format (global): +{ +type= +id= +shell= +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 * + + +- format (note) +{ +trigger= +} +- +*trigger: note velocity from 0 to 127 that triggers the command. Can enter an interval x:y or single value + > optional, default 1:127 + +- format (note) +{ +range= +remap= +float= +} +- +*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 //= diff --git a/example.mim b/example.mim new file mode 100644 index 0000000..e370c51 --- /dev/null +++ b/example.mim @@ -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=[] +} + +] diff --git a/include/Filedat.hpp b/include/Filedat.hpp new file mode 120000 index 0000000..fe943f9 --- /dev/null +++ b/include/Filedat.hpp @@ -0,0 +1 @@ +/home/zawz/code/c/tool/zFiledat/Filedat.hpp \ No newline at end of file diff --git a/include/command.hpp b/include/command.hpp new file mode 100644 index 0000000..dba70c9 --- /dev/null +++ b/include/command.hpp @@ -0,0 +1,38 @@ +#ifndef COMMAND_HPP +#define COMMAND_HPP + +#include + +#include + +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 diff --git a/include/device.hpp b/include/device.hpp new file mode 100644 index 0000000..bb5c113 --- /dev/null +++ b/include/device.hpp @@ -0,0 +1,38 @@ +#ifndef DEVICE_HPP +#define DEVICE_HPP + +#include +#include +#include + +#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 noteCommands[128]; + std::vector ctrlCommands[128]; + // std::vector systCommands; + + std::thread thread; +private: + static void loop(Device* dev); +}; + +extern std::vector device_list; + +#endif //DEVICE_HPP diff --git a/include/options.hpp b/include/options.hpp new file mode 120000 index 0000000..705cd32 --- /dev/null +++ b/include/options.hpp @@ -0,0 +1 @@ +/home/zawz/code/c/tool/zOptions/options.hpp \ No newline at end of file diff --git a/include/stringTools.hpp b/include/stringTools.hpp new file mode 120000 index 0000000..40550b3 --- /dev/null +++ b/include/stringTools.hpp @@ -0,0 +1 @@ +/home/zawz/code/c/tool/stringTools/stringTools.hpp \ No newline at end of file diff --git a/include/system.hpp b/include/system.hpp new file mode 100644 index 0000000..f874692 --- /dev/null +++ b/include/system.hpp @@ -0,0 +1,8 @@ +#ifndef SYSTEM_HPP +#define SYSTEM_HPP + +void device_check(); + +void announce_loop(); + +#endif //SYSTEM_HPP diff --git a/makefile b/makefile new file mode 100644 index 0000000..686b44d --- /dev/null +++ b/makefile @@ -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) diff --git a/src/Filedat.cpp b/src/Filedat.cpp new file mode 120000 index 0000000..fa08719 --- /dev/null +++ b/src/Filedat.cpp @@ -0,0 +1 @@ +/home/zawz/code/c/tool/zFiledat/Filedat.cpp \ No newline at end of file diff --git a/src/command.cpp b/src/command.cpp new file mode 100644 index 0000000..5fbef82 --- /dev/null +++ b/src/command.cpp @@ -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; +} diff --git a/src/device.cpp b/src/device.cpp new file mode 100644 index 0000000..ec9d01c --- /dev/null +++ b/src/device.cpp @@ -0,0 +1,289 @@ +#include "device.hpp" + +#include "stringTools.hpp" + +#include +#include +#include + +#include + +#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_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 ; inoteCommands[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); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..59f312d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,79 @@ +#include +#include + +#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 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 ; iimport_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; +} diff --git a/src/options.cpp b/src/options.cpp new file mode 120000 index 0000000..e62411d --- /dev/null +++ b/src/options.cpp @@ -0,0 +1 @@ +/home/zawz/code/c/tool/zOptions/options.cpp \ No newline at end of file diff --git a/src/stringTools.cpp b/src/stringTools.cpp new file mode 120000 index 0000000..39cd6ce --- /dev/null +++ b/src/stringTools.cpp @@ -0,0 +1 @@ +/home/zawz/code/c/tool/stringTools/stringTools.cpp \ No newline at end of file diff --git a/src/system.cpp b/src/system.cpp new file mode 100644 index 0000000..fefb765 --- /dev/null +++ b/src/system.cpp @@ -0,0 +1,61 @@ +#include "system.hpp" + +#include "device.hpp" + +#include +#include +#include + +#include +#include + +#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); +}