├── CHANGELOG.md ├── res ├── icons │ ├── m2i.xcf │ ├── m2i-black.png │ ├── m2i-dark.png │ ├── m2i-light.png │ ├── m2i-white.png │ └── CMakeLists.txt └── applications │ └── m2i.desktop ├── INSTALL.archlinux.md ├── .gitignore ├── cfg ├── config.lua ├── basic.lua ├── Denon-DN-SC2000.lua └── ddj-wego.lua ├── src ├── util.h ├── x11.h ├── midi.cpp ├── midi.h ├── trayicon.h ├── inotify.h ├── alsa.h ├── jack.h ├── uinput.h ├── util.cpp ├── trayicon.cpp ├── lua.h ├── CMakeLists.txt ├── x11.cpp ├── jack.cpp ├── inotify.cpp ├── uinput.cpp ├── lua.cpp ├── main.cpp └── alsa.cpp ├── INSTALL.md ├── doc ├── docs.cmake ├── CMakeLists.txt └── m2i.1.in ├── LICENCE.md ├── pkg └── PKGBUILD ├── INSTALL.debian-bullseye.md ├── CMakeLists.txt ├── README.md ├── Diary.md └── include └── argh.h /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | midi2input v0.1 2 | * no changes 3 | -------------------------------------------------------------------------------- /res/icons/m2i.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enetheru/midi2input/HEAD/res/icons/m2i.xcf -------------------------------------------------------------------------------- /res/icons/m2i-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enetheru/midi2input/HEAD/res/icons/m2i-black.png -------------------------------------------------------------------------------- /res/icons/m2i-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enetheru/midi2input/HEAD/res/icons/m2i-dark.png -------------------------------------------------------------------------------- /res/icons/m2i-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enetheru/midi2input/HEAD/res/icons/m2i-light.png -------------------------------------------------------------------------------- /res/icons/m2i-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enetheru/midi2input/HEAD/res/icons/m2i-white.png -------------------------------------------------------------------------------- /INSTALL.archlinux.md: -------------------------------------------------------------------------------- 1 | Install Instructions for Archlinux 2 | ---------------------------------- 3 | ```bash 4 | mkdir m2i 5 | curl https://gitlab.com/enetheru/midi2input/raw/master/pkg/PKGBUILD > m2i/PKGBUILD 6 | cd m2i 7 | makepkg -si 8 | ``` 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *.swp 3 | *.pyc 4 | .ycm* 5 | 6 | \.idea/ 7 | 8 | cmake-build-debug/ 9 | 10 | \.kdev4/ 11 | 12 | *.kdev4 13 | 14 | *.kate-swp 15 | 16 | include/ 17 | 18 | pkg/ 19 | 20 | *.srctrlprj 21 | 22 | *.srctrlbm 23 | 24 | *.srctrldb 25 | -------------------------------------------------------------------------------- /cfg/config.lua: -------------------------------------------------------------------------------- 1 | config = { 2 | script = 'basic.lua', 3 | loglevel = 2, 4 | use_alsa = true, 5 | use_jack = false, 6 | reconnect = true, 7 | loop_enabled = true, 8 | main_freq = 25, 9 | loop_freq = 250, 10 | watch_freq = 2500, 11 | } 12 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by enetheru on 9/26/17. 3 | // 4 | 5 | #ifndef M2I_UTIL_H 6 | #define M2I_UTIL_H 7 | #include 8 | 9 | namespace m2i { 10 | std::filesystem::path getPath( std::filesystem::path path ); 11 | }//namespace m2i 12 | 13 | #endif //M2I_UTIL_H 14 | -------------------------------------------------------------------------------- /res/applications/m2i.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=m2i 3 | GenericName=Midi Script Interface 4 | Comment=Translate midi events into mouse and mouse events 5 | Type=Application 6 | Categories=Utility;Midi;Accessibility;TrayIcon 7 | Exec=/usr/bin/m2i -q 8 | Icon=m2i-light 9 | Terminal=false 10 | -------------------------------------------------------------------------------- /src/x11.h: -------------------------------------------------------------------------------- 1 | #ifndef X11_H 2 | #define X11_H 3 | 4 | #include 5 | #include 6 | 7 | namespace m2i { 8 | int XErrorCatcher( Display *disp, XErrorEvent *xe ); 9 | Window XGetParent( Display *xdp, Window w ); 10 | std::string XDetectWindow( Display *xdp ); 11 | } 12 | 13 | #endif //X11_H 14 | -------------------------------------------------------------------------------- /src/midi.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "midi.h" 3 | 4 | std::string 5 | midi_event::str() const 6 | { 7 | return fmt::format( FMT_STRING("{:#04x}, {:#04x}, {:3d}"), status, data1, data2 ); 8 | } 9 | 10 | 11 | std::ostream& operator<<( std::ostream &os, const midi_event &event ){ 12 | return os << event.str(); 13 | } 14 | -------------------------------------------------------------------------------- /src/midi.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by enetheru on 9/26/17. 3 | // 4 | 5 | #ifndef M2I_MIDI_H 6 | #define M2I_MIDI_H 7 | 8 | #include 9 | 10 | //simple midi event structure. 11 | struct midi_event { 12 | unsigned char status; 13 | unsigned char data1; 14 | unsigned char data2; 15 | 16 | std::string str() const; 17 | private: 18 | }; 19 | 20 | #endif //M2I_MIDI_H 21 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | Generic Install Instructions 2 | ------- 3 | Install dependencies: 4 | 5 | * build time: git cmake spdlog, fmt, openimageio gtk-update-icon-cache 6 | * required: spdlog, lua 5.3, alsa-lib, libx11 7 | * Optional: jack | jack2, qt5-core, qt-widgets 8 | 9 | ```bash 10 | git clone https://gitlab.com/enetheru/midi2input.git 11 | cd midi2input 12 | mkdir build 13 | cd build 14 | cmake ../midi2input/ 15 | make 16 | make install 17 | gtk-update-icon-cache -q -t -f usr/share/icons/hicolor 18 | ``` 19 | -------------------------------------------------------------------------------- /src/trayicon.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef M2I_TRAYICON_H 3 | #define M2I_TRAYICON_H 4 | #include 5 | #include 6 | 7 | QT_BEGIN_NAMESPACE 8 | class QAction; 9 | class QMenu; 10 | QT_END_NAMESPACE 11 | 12 | class m2iTrayIcon : public QDialog 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | m2iTrayIcon(); 18 | 19 | void setVisible(bool visible) override; 20 | 21 | private slots: 22 | void quit(); 23 | 24 | private: 25 | void createActions(); 26 | void createTrayIcon(); 27 | 28 | QAction *quitAction{nullptr}; 29 | QSystemTrayIcon *trayIcon{nullptr}; 30 | QMenu *trayIconMenu{nullptr}; 31 | }; 32 | 33 | #endif//M2I_TRAYICON_H 34 | -------------------------------------------------------------------------------- /doc/docs.cmake: -------------------------------------------------------------------------------- 1 | # NOTE: cant cache multi line strings in cmake, so just include this file 2 | # whenever you need these strings 3 | 4 | set( DOC_TITLE "M2I" ) 5 | set( DOC_NAME "m2i" ) 6 | string( TIMESTAMP DOC_DATE %Y-%m-%d ) 7 | 8 | set( DOC_DESCRIPTION_SHORT 9 | "midi input -> keyboard|mouse|midi|command output" ) 10 | 11 | set( DOC_DESCRIPTION 12 | "${DOC_NAME}(midi to input) is a small service like applicatin that runs 13 | scripted actions in response to to midi events. Actions can be mouse, 14 | keyboard events, commands and more midi events. ${DOC_NAME} can receive midi 15 | events from either the ALSA and or Jack midi sequenser." ) 16 | 17 | set( DOC_AUTHOR "Samuel Nicholas and contributors" ) 18 | -------------------------------------------------------------------------------- /src/inotify.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef M2I_INOTIFY_H 3 | #define M2I_INOTIFY_H 4 | #include 5 | 6 | #include 7 | 8 | namespace m2i { 9 | 10 | struct watcher { 11 | int wd; 12 | std::filesystem::path path; 13 | void (*function)(); 14 | }; 15 | 16 | class Notifier { 17 | public: 18 | Notifier(); 19 | ~Notifier(); 20 | int watchPath( watcher ); 21 | void check(); 22 | explicit operator bool() const { return fd != -1; } 23 | private: 24 | void handleEvents(); 25 | int fd = -1; // file descriptor for accessing the inotify API 26 | std::vector< watcher > watchers; 27 | pollfd pfd{}; //file descriptor to pass to poll 28 | }; 29 | 30 | } //end namespace m2i 31 | #endif//M2I_INOTIFY_H 32 | -------------------------------------------------------------------------------- /src/alsa.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDI2INPUT_ALSA_H 2 | #define MIDI2INPUT_ALSA_H 3 | 4 | //header to initialise jack interface and keep it clean 5 | #include 6 | #include "midi.h" 7 | #include "util.h" 8 | 9 | class AlsaSeq { 10 | public: 11 | AlsaSeq() = default; 12 | int open(); 13 | void close(); 14 | 15 | int connect( const std::string &client_name, const std::string &port_name ); 16 | 17 | midi_event event_receive(); 18 | int event_pending(); 19 | void event_send( const midi_event &event ); 20 | 21 | explicit operator bool() const { return seq; } 22 | 23 | ~AlsaSeq(); 24 | private: 25 | int iport_id = -1; 26 | int oport_id = -1; 27 | int client_id = -1; 28 | snd_seq_t *seq = nullptr; 29 | }; 30 | 31 | #endif // MIDI2INPUT_JACK_H 32 | -------------------------------------------------------------------------------- /src/jack.h: -------------------------------------------------------------------------------- 1 | #ifndef MIDI2INPUT_JACK_H 2 | #define MIDI2INPUT_JACK_H 3 | 4 | //header to initialise jack interface and keep it clean 5 | #include 6 | #include 7 | #include 8 | #include "midi.h" 9 | 10 | class JackSeq{ 11 | public: 12 | JackSeq() = default; 13 | void init(); 14 | void fina(); 15 | 16 | int do_process(jack_nframes_t num_frames); 17 | void event_send( const midi_event &event ); 18 | unsigned long event_pending(); 19 | midi_event event_receive(); 20 | 21 | const bool &valid = valid_; 22 | 23 | ~JackSeq(); 24 | private: 25 | bool valid_ = false; 26 | 27 | jack_client_t *client = nullptr; 28 | jack_port_t *input_port = nullptr; 29 | jack_port_t *output_port = nullptr; 30 | 31 | jack_ringbuffer_t *event_buf = nullptr; 32 | 33 | }; 34 | 35 | #endif // MIDI2INPUT_JACK_H 36 | -------------------------------------------------------------------------------- /src/uinput.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by enetheru on 22/9/20. 3 | // 4 | 5 | #ifndef M2I_UINPUT_H 6 | #define M2I_UINPUT_H 7 | 8 | // the event codes are derived from /usr/include/linux/input-event-codes.h 9 | 10 | namespace m2i { 11 | // This class exists to manage the virtual uinput device 12 | class Uinput { 13 | int fd; 14 | struct libevdev_uinput *uidev{}; 15 | public: 16 | Uinput(); 17 | ~Uinput(); 18 | 19 | //emulating input devices 20 | void keypress( int input_event_code ); 21 | void keydown( int input_event_code ); 22 | void keyup( int input_event_code ); 23 | void mousemove( int rel_x, int rel_y ); 24 | void mousewarp( int abs_x, int abs_y ); 25 | void mousescroll( int distance ); 26 | void mousehscroll( int distance ); 27 | }; 28 | 29 | }// namespace m2i 30 | #endif //M2I_UINPUT_H -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | project clearly links to open source projects 2 | 3 | * ALSA is released under the GPL (GNU General Public license) and the LGPL (GNU Lesser General Public License). 4 | * JACK is under the terms of the GNU GPL and LGPL licenses. 5 | * adishavit/argh is released under BSD 3-clause "New" or "Revised" License. 6 | * fmtlib is relased under BSD 2-clause "Simplified" License. 7 | * spdlog is released under the MIT Licence. 8 | * linking against the stdlib is exempted from having to be GPL. 9 | * system calls like inotify are considered normal use: http://inotify.aiken.cz/?section=inotify&page=faq 10 | * Qt is released under GPL & LGPLv3 licenses. 11 | * xlib looks like bsd 3-clause or similar. 12 | 13 | That being the case, looks like LGPL is the most restrictive required licence. 14 | 15 | And since this is an end user program which I dont imagine anyone would ever 16 | link to I'll go with the GPLv2 for my parts. -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "util.h" 4 | 5 | namespace fs = std::filesystem; 6 | 7 | namespace m2i { 8 | fs::path 9 | getPath( fs::path path ) 10 | { 11 | //file doesnt exist but the path is relative then it might exist in .config 12 | if( not fs::exists( path ) and path.is_relative() ){ 13 | if( getenv( "XDG_CONFIG_HOME" ) ) 14 | path = std::string( getenv( "XDG_CONFIG_HOME") ) + "/m2i/" + path.string(); 15 | else 16 | path = std::string( getenv( "HOME") ) + "/.config/m2i/" + path.string(); 17 | } 18 | 19 | //possible it could be a symlink 20 | if( fs::exists( path ) and fs::is_symlink( path ) ) 21 | path = fs::read_symlink( path ); 22 | //Note: this only gives one deference, might want more 23 | 24 | if( !fs::exists( path ) )return fs::path();//file simply doesnt exist 25 | 26 | return path; 27 | } 28 | 29 | }//namespace m2i 30 | -------------------------------------------------------------------------------- /doc/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include( docs.cmake ) 2 | 3 | if( BUILD_DOCS ) 4 | configure_file( m2i.1.in m2i.1 ) 5 | 6 | add_custom_target( docs ALL 7 | DEPENDS m2i m2i.1.gz changelog.gz ) 8 | 9 | #./debian/copyright 10 | install( FILES ${CMAKE_SOURCE_DIR}/LICENCE.md 11 | DESTINATION share/doc/m2i 12 | COMPONENT applications 13 | RENAME copyright ) 14 | 15 | # Man Page 16 | add_custom_command( OUTPUT m2i.1.gz 17 | COMMAND gzip -9 m2i.1 ) 18 | 19 | install( FILES ${CMAKE_CURRENT_BINARY_DIR}/m2i.1.gz 20 | DESTINATION share/man/man1/ 21 | COMPONENT applications ) 22 | 23 | #./debian/changelog.gz 24 | add_custom_command( OUTPUT changelog.gz 25 | COMMAND cp ${CMAKE_SOURCE_DIR}/CHANGELOG.md ${CMAKE_CURRENT_BINARY_DIR}/changelog 26 | COMMAND gzip -9 changelog ) 27 | 28 | install( FILES ${CMAKE_CURRENT_BINARY_DIR}/changelog.gz 29 | DESTINATION share/doc/m2i 30 | COMPONENT applications ) 31 | 32 | endif() 33 | -------------------------------------------------------------------------------- /src/trayicon.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "trayicon.h" 5 | #include "util.h" 6 | 7 | namespace m2i { 8 | extern bool quit; 9 | } 10 | 11 | m2iTrayIcon::m2iTrayIcon() 12 | { 13 | createActions(); 14 | createTrayIcon(); 15 | 16 | trayIcon->show(); 17 | 18 | setWindowTitle(tr("About m2i")); 19 | resize(400, 300); 20 | } 21 | 22 | void 23 | m2iTrayIcon::setVisible( bool visible ) 24 | { 25 | QDialog::setVisible( visible ); 26 | } 27 | 28 | void 29 | m2iTrayIcon::quit(){ 30 | m2i::quit = true; 31 | } 32 | 33 | void 34 | m2iTrayIcon::createActions() 35 | { 36 | quitAction = new QAction(tr("&Quit"), this); 37 | connect(quitAction, &QAction::triggered, this, &m2iTrayIcon::quit ); 38 | } 39 | 40 | void 41 | m2iTrayIcon::createTrayIcon() 42 | { 43 | trayIconMenu = new QMenu(this); 44 | trayIconMenu->addAction( quitAction ); 45 | 46 | trayIcon = new QSystemTrayIcon(this); 47 | trayIcon->setContextMenu(trayIconMenu); 48 | 49 | QIcon::setThemeName( "hicolor" ); 50 | trayIcon->setIcon( QIcon::fromTheme("m2i-light") ); 51 | } 52 | -------------------------------------------------------------------------------- /res/icons/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #generate any icons we need to here 2 | 3 | # the existance of prefix/share/icons/hicolour is specified in the xdg spec. 4 | add_custom_target( icons ALL 5 | DEPENDS m2i ) 6 | 7 | add_custom_command( TARGET icons POST_BUILD 8 | COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/hicolor/48x48/apps 9 | COMMAND oiiotool -i ${CMAKE_CURRENT_SOURCE_DIR}/m2i-black.png --resize 48x48 -o ${CMAKE_CURRENT_BINARY_DIR}/hicolor/48x48/apps/m2i-black.png 10 | COMMAND oiiotool -i ${CMAKE_CURRENT_SOURCE_DIR}/m2i-dark.png --resize 48x48 -o ${CMAKE_CURRENT_BINARY_DIR}/hicolor/48x48/apps/m2i-dark.png 11 | COMMAND oiiotool -i ${CMAKE_CURRENT_SOURCE_DIR}/m2i-light.png --resize 48x48 -o ${CMAKE_CURRENT_BINARY_DIR}/hicolor/48x48/apps/m2i-light.png 12 | COMMAND oiiotool -i ${CMAKE_CURRENT_SOURCE_DIR}/m2i-white.png --resize 48x48 -o ${CMAKE_CURRENT_BINARY_DIR}/hicolor/48x48/apps/m2i-white.png 13 | COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/hicolor/512x512/apps 14 | COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/m2i-light.png ${CMAKE_CURRENT_BINARY_DIR}/hicolor/512x512/apps 15 | COMMENT "Generating Icons") 16 | 17 | install( DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/hicolor DESTINATION share/icons ) 18 | -------------------------------------------------------------------------------- /pkg/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Samuel Nicholas 2 | pkgname='m2i-git' 3 | pkgver=r237.e5ee083 4 | pkgrel=1 5 | pkgdesc="turn midi events into mouse and keyboard events" 6 | arch=( 'x86_64' 'i686' 'armv7h' ) 7 | url="https://gitlab.com/enetheru/midi2input" 8 | license=('GPL3') 9 | groups=() 10 | depends=( 'fmt' 'spdlog' 'lua53' 'qt5-base' 'jack' 'hicolor-icon-theme' ) 11 | makedepends=( 'git' 'cmake' 'openimageio' 'gtk-update-icon-cache' 12 | 'desktop-file-utils' ) 13 | provides=('m2i') 14 | conflicts=('m2i') 15 | replaces=() 16 | backup=() 17 | options=() 18 | install= 19 | source=('git+https://gitlab.com/enetheru/midi2input.git') 20 | noextract=() 21 | md5sums=('SKIP') 22 | 23 | pkgver() { 24 | cd "$srcdir/midi2input" 25 | # printf "%s" "$(git describe --long | sed 's/\([^-]*-\)g/r\1/;s/-/./g')" 26 | printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" 27 | } 28 | 29 | prepare() { 30 | cd "$srcdir/midi2input" 31 | git submodule update --init 32 | } 33 | 34 | build() { 35 | cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr "$srcdir/midi2input" 36 | make 37 | } 38 | 39 | check() { 40 | # cd "$srcdir/midi2input" 41 | # make -k check 42 | echo skipping 43 | } 44 | 45 | package() { 46 | make DESTDIR="$pkgdir/" install 47 | } 48 | -------------------------------------------------------------------------------- /INSTALL.debian-bullseye.md: -------------------------------------------------------------------------------- 1 | Installation Instructions for Debian Bullseye 2 | --------------------------------------------- 3 | I've only tested this in a VM and haven't used it for anything. 4 | Currently the qt cmake find scripts didn't detect the qt core and widgets 5 | libraries which prevents the use of the system tray icon, not sure what's up 6 | with that, could use some help debugging. 7 | 8 | ```bash 9 | apt-get install git cmake g+ lua5.3-dev libasound2-dev libspdlog1 libfmt-dev 10 | - OPTIONALLY libjack-dev 11 | git clone https://gitlab.com/enetheru/midi2input.git 12 | cd midi2input 13 | mkdir build 14 | cd build 15 | cmake ../midi2input/ 16 | make 17 | ``` 18 | You can just `make install` to get it on your system but you should generate a 19 | package so that it can be updated and removed using the install manifest for 20 | clean system operation.. However, the dependencies in the CMakeLists.txt 21 | listed in the CPack section are out of date for bullseye and need editing. 22 | 23 | The problem is that it depends on how you configured the application too, and i 24 | haven't setup the CMakeLists to properly consider how that operates. 25 | 26 | For my testing purposes I changed 27 | 28 | ```cmake 29 | set( CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libspdlog1, libx11-6, jackd1, liblua5.3-0" ) 30 | ``` 31 | 32 | I could then build the package and install it as below 33 | 34 | ```bash 35 | make package 36 | sudo dpkg -i m2i-*-Linux.deb 37 | ``` 38 | -------------------------------------------------------------------------------- /src/lua.h: -------------------------------------------------------------------------------- 1 | #ifndef M2I_LUA_H 2 | #define M2I_LUA_H 3 | 4 | #include 5 | 6 | extern "C" { 7 | #include 8 | #include 9 | #include 10 | } 11 | 12 | #include "util.h" 13 | #include "midi.h" 14 | 15 | namespace m2i { 16 | /* functions register in the lua environment are named with lua_ */ 17 | 18 | lua_State *register_lua_funcs(lua_State *L); 19 | int midi_to_lua(lua_State *L, const midi_event &event ); 20 | 21 | // m2i functions 22 | int lua_print( lua_State *L ); 23 | int lua_midisend( lua_State *L ); 24 | int lua_exec( lua_State *L ); 25 | int lua_quit( lua_State *L ); 26 | int lua_loopenable( lua_State *L ); 27 | int lua_milliseconds( lua_State *L ); 28 | 29 | //uinput events 30 | int lua_keypress( lua_State *L ); 31 | int lua_keydown( lua_State *L ); 32 | int lua_keyup( lua_State *L ); 33 | int lua_mousemove( lua_State *L ); 34 | int lua_mousewarp(lua_State *L ); 35 | int lua_mousescroll( lua_State *L ); 36 | int lua_mousehscroll( lua_State *L ); 37 | 38 | // X11 functions 39 | #ifdef WITH_XORG 40 | int lua_detectwindow( lua_State *L ); 41 | #endif//WITH_XORG 42 | 43 | #ifdef WITH_ALSA 44 | // ALSA functions 45 | int lua_alsaconnect( lua_State *L ); 46 | #endif//WITH_ALSA 47 | 48 | #ifdef WITH_JACK 49 | // Jack functions 50 | int lua_jackconnect( lua_State *L ); 51 | #endif//WITH_JACK 52 | 53 | }//end namespace m2i 54 | #endif//M2I_LUA_H 55 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable( m2i 2 | main.cpp 3 | util.h util.cpp 4 | midi.h midi.cpp 5 | lua.h lua.cpp 6 | inotify.h inotify.cpp 7 | uinput.h uinput.cpp) 8 | 9 | target_include_directories( m2i PRIVATE 10 | ${LUA_INCLUDE_DIR} 11 | ${CMAKE_CURRENT_BINARY_DIR} 12 | ${PROJECT_SOURCE_DIR}/include 13 | ${EVDEV_INCLUDE_DIR}) 14 | 15 | target_link_libraries( m2i PRIVATE 16 | fmt::fmt 17 | spdlog::spdlog 18 | stdc++fs 19 | ${LUA_LIBRARIES} 20 | ${EVDEV_LIBRARY}) 21 | 22 | if( ${ALSA_FOUND} ) 23 | target_sources( m2i PRIVATE alsa.h alsa.cpp ) 24 | target_include_directories( m2i PRIVATE ${ALSA_INCLUDE_DIRS} ) 25 | target_link_libraries( m2i PRIVATE ${ALSA_LIBRARIES} ) 26 | add_definitions( -DWITH_ALSA ) 27 | endif() 28 | 29 | if( ${JACK_FOUND} ) 30 | target_sources( m2i PRIVATE jack.h jack.cpp ) 31 | target_include_directories( m2i PRIVATE ${JACK_INCLUDE_DIRS} ) 32 | target_link_libraries( m2i PRIVATE ${JACK_LIBRARIES} ) 33 | add_definitions( -DWITH_JACK ) 34 | endif() 35 | 36 | if( ${WITH_X11} ) 37 | target_sources( m2i PRIVATE x11.h x11.cpp ) 38 | target_include_directories( m2i PRIVATE ${X11_INCLUDE_DIRS} ) 39 | target_link_libraries( m2i PRIVATE ${X11_LIBRARIES} ) 40 | add_definitions( -DWITH_XORG ) 41 | endif() 42 | 43 | if( ${Qt5Core_FOUND} AND ${Qt5Widgets_FOUND} ) 44 | target_sources( m2i PRIVATE trayicon.h trayicon.cpp ) 45 | target_link_libraries( m2i PUBLIC Qt5::Widgets ) 46 | add_definitions( -DWITH_QT ) 47 | endif() 48 | 49 | target_compile_options( m2i PRIVATE -Wall -Wextra -Wshadow -Wnon-virtual-dtor -pedantic ) 50 | 51 | target_compile_features( m2i PRIVATE 52 | cxx_range_for 53 | cxx_nullptr 54 | cxx_auto_type ) 55 | 56 | set_target_properties( m2i PROPERTIES 57 | RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR} ) 58 | 59 | if( BUILD_DOCS ) 60 | add_custom_command( TARGET m2i POST_BUILD 61 | COMMAND ${PROJECT_BINARY_DIR}/m2i -h > ${CMAKE_BINARY_DIR}/doc/help.txt 62 | COMMENT "m2i -h > ${CMAKE_BINARY_DIR}/doc/help.txt" ) 63 | endif() 64 | 65 | install( TARGETS m2i DESTINATION bin ) 66 | -------------------------------------------------------------------------------- /cfg/basic.lua: -------------------------------------------------------------------------------- 1 | --[[ Imported c++ Functions ]]-- 2 | --initialise() 3 | --keypress( XK_keycode ) 4 | --keydown( XK_keycode ) 5 | --keyup( XK_keycode ) 6 | --buttonpress( n ) 7 | --buttondown( n ) 8 | --buttonup( n ) 9 | --mousemove( x, y ) 10 | --mousepos( x, y ) 11 | --midi_send( { status, data1, data2 } ) 12 | --exec( 'command' ) 13 | -- 14 | --[[ Imported Global Variables ]]-- 15 | --wm_class 16 | --autoconnect 17 | -- 18 | --[[ Functions you must create ]] 19 | --midi_recv( status, data1, data2 ) 20 | --loop() 21 | 22 | ------------------------ 23 | 24 | --[[ Simple Example ]]-- 25 | 26 | --[[ Defines ]]-- 27 | -- look to X11\keysymdef.h for the full list 28 | XK_space = 0x0020 --/* U+0020 SPACE */ 29 | 30 | controller = { 31 | deck = { 32 | A = { 33 | play = {0x90, 0x0B, 127} 34 | } 35 | } 36 | } 37 | 38 | --[[ global settings ]]-- 39 | -- autoconnect: can be true, false, or a named jack port. default = true 40 | autoconnect = false 41 | 42 | --[[ Pattern matcher for messages ]]-- 43 | -- 44 | -- Both pattern and message share the same structure: {status, data1, data2}. 45 | -- For any element of the pattern is equal -1, corresponding element of the 46 | -- message is ignored / considered equal. Third element is often used for 47 | -- continuous measurements such as acceleration, thus in addition to being -1, 48 | -- it can also be nil, i.e. omitted entirely, making it for a table of two 49 | -- elements, like this: {0xb0, 0x15} instead of this: {0xb0, 0x15, -1}. 50 | function message_matches( pattern, message ) 51 | if pattern[1] ~= -1 then 52 | if pattern[1] ~= message[1] then return false end 53 | end 54 | if pattern[2] ~= -1 then 55 | if pattern[2] ~= message[2] then return false end 56 | end 57 | if pattern[3] ~= nil and pattern[3] ~= -1 then 58 | if pattern[3] ~= message[3] then return false end 59 | end 60 | return true 61 | end 62 | 63 | --[[ initialisation function ]]-- 64 | -- run immediately after the application launches and connects to the device 65 | function script_init() 66 | print( "nothing to initialise" ) 67 | end 68 | 69 | function loop() 70 | detectwindow() 71 | end 72 | 73 | --[[ Input Event Handler ]]-- 74 | function midi_recv( status, data1, data2 ) 75 | local message = { status, data1, data2 } 76 | 77 | if( message_matches( controller.deck['A'].play, message ) ) then 78 | keypress( XK_space ) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required( VERSION 3.9 ) 2 | 3 | project( m2i ) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | 7 | # Configuration 8 | # ============= 9 | if( NOT CMAKE_BUILD_TYPE ) 10 | set( CMAKE_BUILD_TYPE Release CACHE STRING "The type of build, options are None, Debug, Release, RelWithDebInfo, MinSizeRel" FORCE ) 11 | endif() 12 | 13 | if( NOT DEFINED BUILD_DOCS ) 14 | set( BUILD_DOCS YES CACHE BOOL "Whether or not to build the documentation" FORCE ) 15 | endif() 16 | 17 | # dependencies 18 | # ============ 19 | find_path(EVDEV_INCLUDE_DIR libevdev/libevdev.h PATH_SUFFIXES libevdev-1.0 REQUIRED ) 20 | find_library(EVDEV_LIBRARY NAMES evdev libevdev REQUIRED ) 21 | find_package( spdlog REQUIRED ) 22 | find_package( fmt REQUIRED ) 23 | find_package( Lua 5.3 EXACT REQUIRED ) 24 | find_package( ALSA REQUIRED ) 25 | find_package( X11 ) 26 | find_package( Qt5 QUIET COMPONENTS Core Widgets ) 27 | find_package( PkgConfig REQUIRED ) 28 | pkg_check_modules( JACK jack ) 29 | 30 | if( ${X11_FOUND} ) 31 | set( WITH_X11 YES ) 32 | endif() 33 | 34 | if( ${Qt5Core_FOUND} AND ${Qt5Widgets_FOUND} ) 35 | set(CMAKE_AUTOMOC ON) 36 | add_subdirectory( res/icons ) 37 | endif() 38 | 39 | # messages 40 | message( "-- Configuration:" ) 41 | message( "-- CMAKE_BUILD_TYPE=" ${CMAKE_BUILD_TYPE} ) 42 | message( "-- CMAKE_INSTALL_PREFIX=" ${CMAKE_INSTALL_PREFIX} ) 43 | message( "-- BUILD_DOCS=" ${BUILD_DOCS} ) 44 | message( "-- WITH_ALSA=" ${ALSA_FOUND} ) 45 | message( "-- WITH_JACK=" ${JACK_FOUND} ) 46 | message( "-- WITH_X11=" ${WITH_X11} ) 47 | message( "-- WITH_QT=" ${Qt5_FOUND} ) 48 | 49 | if( NOT ( ${ALSA_FOUND} OR "${JACK_FOUND}" ) ) 50 | message( SEND_ERROR "Either ALSA or Jack needs to be available.") 51 | endif() 52 | 53 | # sub directories 54 | # =============== 55 | add_subdirectory( src ) 56 | 57 | if( ${BUILD_DOCS} ) 58 | add_subdirectory( doc ) 59 | endif() 60 | 61 | # additional installation directives 62 | # ================================== 63 | install( FILES cfg/config.lua cfg/basic.lua 64 | DESTINATION share/m2i ) 65 | install( FILES res/applications/m2i.desktop 66 | DESTINATION share/applications ) 67 | 68 | # CPack 69 | # ===== 70 | include( doc/docs.cmake ) 71 | set( CPACK_STRIP_FILES true ) 72 | set( CPACK_GENERATOR "DEB" ) 73 | set( CPACK_DEBIAN_PACKAGE_VERSION "0.1" ) 74 | # Build dependencies, libsdlog-dev, libfmt-dev, appears to work for debian 75 | # testing 76 | set( CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libspdlog1, libx11-6, libjack-jackd2-0, liblua5.3-0" ) 77 | set( CPACK_DEBIAN_PACKAGE_MAINTAINER "${DOC_AUTHORS}" ) 78 | string( REPLACE "\n" "\n " CPACK_DEBIAN_PACKAGE_DESCRIPTION ${DOC_DESCRIPTION_SHORT} "\n" ${DOC_DESCRIPTION} ) 79 | set( CPACK_DEBIAN_PACKAGE_SECTION "utils" ) 80 | include( CPack ) 81 | -------------------------------------------------------------------------------- /src/x11.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "x11.h" 5 | 6 | namespace m2i { 7 | 8 | /* NOTE this error call back is to avoid an assertion when X11 cant find the 9 | * window properties, which is hardly important for our use case */ 10 | int XErrorCatcher( Display *disp, XErrorEvent *xe ) 11 | { 12 | (void)disp; (void)xe; 13 | spdlog::error( FMT_STRING( "X11: Generic XError Catcher that does nothing" ) ); 14 | return 0; 15 | } 16 | 17 | Window 18 | XGetParent( Display *xdp, Window w ) 19 | { 20 | Window root_return; 21 | Window parent_return; 22 | Window *children_return = nullptr; 23 | unsigned int nchildren_return = 0; 24 | 25 | if( XQueryTree( xdp, w, 26 | &root_return, 27 | &parent_return, 28 | &children_return, 29 | &nchildren_return ) ){ 30 | if( children_return != nullptr ) XFree( children_return ); 31 | if( parent_return != DefaultRootWindow( xdp ) ) 32 | return parent_return; 33 | else return None; 34 | } 35 | else { 36 | spdlog::error( FMT_STRING( "X11: Failed XQueryTree" ) ); 37 | } 38 | return None; 39 | } 40 | 41 | std::string 42 | XDetectWindow( Display* xdp ) 43 | { 44 | static Window w_current; 45 | 46 | if(! xdp ){ 47 | spdlog::error( FMT_STRING( "X11: invalid handle to X display" ) ); 48 | return "None"; 49 | } 50 | 51 | Window w; 52 | int revert_to; 53 | XGetInputFocus( xdp, &w, &revert_to ); 54 | if( w == None ) return "None"; 55 | if( w == w_current ) return std::string(); 56 | else w_current = w; 57 | 58 | while( true ){ 59 | Atom property = XInternAtom( xdp, "WM_CLASS", False ); 60 | if( property == None ){ 61 | spdlog::error( FMT_STRING( "X11: Failed XInternAtom for WM_CLASS" ) ); 62 | break; 63 | } 64 | 65 | Atom actual_type_return; 66 | int actual_format_return = 0; 67 | unsigned long nitems_return = 0L; 68 | unsigned long bytes_after_return = 0L; 69 | unsigned char *prop_return = nullptr; 70 | 71 | if( XGetWindowProperty( 72 | xdp, w, property, 0L, 1024L, False, XA_STRING, 73 | &actual_type_return, 74 | &actual_format_return, 75 | &nitems_return, 76 | &bytes_after_return, 77 | &prop_return ) == Success ) 78 | { 79 | if( actual_type_return == XA_STRING 80 | && actual_format_return == 8 ){ 81 | std::string retval( reinterpret_cast< const char*>( prop_return ) ); 82 | XFree( prop_return ); 83 | spdlog::info( FMT_STRING( "X11: WM_CLASS: {}" ), retval ); 84 | return retval; 85 | } 86 | if( actual_format_return == None ) 87 | spdlog::warn( FMT_STRING( "X11: property: WM_CLASS not present" ) ); 88 | } 89 | else{ 90 | spdlog::error( FMT_STRING( "X11: XGetWindowProperty did not return Success" ) ); 91 | return "None"; 92 | } 93 | 94 | w = XGetParent( xdp, w ); 95 | } 96 | return "None"; 97 | } 98 | 99 | }// end namespace m2i 100 | -------------------------------------------------------------------------------- /src/jack.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "jack.h" 4 | 5 | #define EVENT_BUF_SIZE 64 6 | 7 | void 8 | JackSeq::init() 9 | { 10 | spdlog::info( FMT_STRING( "JACK: Initialising" ) ); 11 | 12 | event_buf = jack_ringbuffer_create(EVENT_BUF_SIZE * sizeof(midi_event)); 13 | 14 | jack_set_error_function( [](const char *msg){ 15 | spdlog::error( FMT_STRING( "JACK: {}" ), msg ); 16 | } ); 17 | 18 | jack_set_info_function( [](const char *msg){ 19 | spdlog::info( FMT_STRING( "JACK: {}" ), msg ); 20 | } ); 21 | 22 | client = jack_client_open( "midi2input_jack", JackNoStartServer, nullptr ); 23 | if(! client ){ 24 | spdlog::info( FMT_STRING( "JACK: unable to open client on server" ) ); 25 | return; 26 | } 27 | 28 | jack_set_process_callback( client, [](jack_nframes_t nframes, void *arg){ 29 | auto jackSeq = (JackSeq*) arg; 30 | return jackSeq->do_process(nframes); 31 | }, this); 32 | 33 | spdlog::info( FMT_STRING( "JACK: registering ports" ) ); 34 | input_port = jack_port_register( client, "in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0 ); 35 | output_port = jack_port_register( client, "out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0 ); 36 | 37 | spdlog::info( FMT_STRING( "JACK: Activating client" ) ); 38 | if( jack_activate( client ) ){ 39 | spdlog::error( FMT_STRING( "JACK: cannot activate client" ) ); 40 | return; 41 | } 42 | valid_ = true; 43 | } 44 | 45 | void 46 | JackSeq::fina() 47 | { 48 | if( valid ){ 49 | valid_ = false; 50 | jack_client_close( client ); 51 | jack_ringbuffer_free(event_buf); 52 | } 53 | } 54 | 55 | 56 | void 57 | JackSeq::event_send( const midi_event &event ) 58 | { 59 | //FIXME there is a bug in here somewhere that makes sending a midi event to the 60 | //controller fail. 61 | void *port_buf = jack_port_get_buffer( output_port, 0 ); 62 | if(! port_buf ){ 63 | spdlog::error( FMT_STRING( "JACK: Cannot send events with no connected ports" ) ); 64 | return; 65 | } 66 | jack_midi_clear_buffer( port_buf ); 67 | 68 | jack_midi_data_t *mdata = jack_midi_event_reserve( port_buf, 0, 3 ); 69 | 70 | mdata[0] = event.status; 71 | mdata[1] = event.data1; 72 | mdata[2] = event.data2; 73 | jack_midi_event_write( port_buf, 0, mdata, 3 ); 74 | 75 | spdlog::info( FMT_STRING( "JACK: midi-send: {}" ), event.str() ); 76 | } 77 | 78 | // this is a callback passed to jack to read events into our buffer 79 | int 80 | JackSeq::do_process(jack_nframes_t process_nframes) 81 | { 82 | void *port_buf = jack_port_get_buffer( input_port, process_nframes ); 83 | uint32_t jack_event_count = jack_midi_get_event_count( port_buf ); 84 | 85 | if (jack_event_count > 0) { 86 | spdlog::info( FMT_STRING( "JACK: Event Count: {}" ), jack_event_count ); 87 | } 88 | 89 | uint32_t i; 90 | jack_midi_event_t jack_event; 91 | midi_event m2i_event{}; 92 | for (i = 0; i < jack_event_count; i++) { 93 | int ret = jack_midi_event_get(&jack_event, port_buf, i); 94 | 95 | if (ret != 0) { continue; } 96 | 97 | if (jack_ringbuffer_write_space(event_buf) < sizeof(midi_event)) { 98 | spdlog::error( FMT_STRING( "JACK: Unable to read event from jack, ringbuffer was full. Skipping event." ) ); 99 | continue; 100 | } 101 | 102 | m2i_event.status = jack_event.buffer[0]; 103 | m2i_event.data1 = jack_event.buffer[1]; 104 | m2i_event.data2 = jack_event.buffer[2]; 105 | 106 | jack_ringbuffer_write(event_buf, (const char*) &m2i_event, sizeof(m2i_event)); 107 | } 108 | 109 | return 0; 110 | } 111 | 112 | unsigned long 113 | JackSeq::event_pending() 114 | { 115 | return jack_ringbuffer_read_space(event_buf) / sizeof(midi_event); 116 | } 117 | 118 | midi_event 119 | JackSeq::event_receive() 120 | { 121 | midi_event result{}; 122 | auto bytes_read = jack_ringbuffer_read(event_buf, (char*) &result, sizeof(result)); 123 | if (bytes_read == 0) { 124 | spdlog::error( FMT_STRING( "JACK: Ringbuffer read didn't read." ) ); 125 | result = {}; 126 | return result; 127 | } else if (bytes_read < sizeof(result)) { 128 | spdlog::warn( FMT_STRING( "JACK: Ringbuffer read an incomplete event, uh oh" ) ); 129 | result = {}; 130 | return result; 131 | } 132 | 133 | spdlog::info( FMT_STRING( "JACK: midi-recv: {}" ), result.str() ); 134 | return result; 135 | } 136 | 137 | JackSeq::~JackSeq(){ 138 | fina(); 139 | } 140 | -------------------------------------------------------------------------------- /src/inotify.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | namespace fs = std::experimental::filesystem; 9 | 10 | #include 11 | 12 | #include "inotify.h" 13 | 14 | namespace m2i { 15 | 16 | Notifier::Notifier(){ 17 | /* Create the file descriptor for accessing the inotify API */ 18 | fd = inotify_init1( IN_NONBLOCK ); 19 | if( fd == -1 ){ 20 | spdlog::error( FMT_STRING( "INOTIFY: init1" ) ); 21 | return; 22 | } 23 | /* Prepare for polling */ 24 | pfd = { fd, POLLIN, 0 }; 25 | } 26 | 27 | Notifier::~Notifier(){ 28 | /* Close inotify file descriptor */ 29 | if( close( fd ) != 0){ 30 | spdlog::error( FMT_STRING( "INOTIFY: close" ) ); 31 | return; 32 | } 33 | } 34 | 35 | int 36 | Notifier::watchPath( watcher input ){ 37 | if( input.path.empty() ){ 38 | spdlog::error( FMT_STRING( "INOTIFY: empty path" ) ); 39 | return -1; 40 | } 41 | auto path = input.path; 42 | 43 | /* Mark directories for events 44 | - file was opened 45 | - file was closed */ 46 | //wd = watch descriptor 47 | input.wd = inotify_add_watch( 48 | fd, input.path.parent_path().c_str(), 49 | IN_CLOSE_WRITE | IN_MODIFY 50 | ); 51 | if( input.wd == -1 ){ 52 | spdlog::error( FMT_STRING( "INOTIFY: Cannot watch {}" ), path.c_str() ); 53 | return -1; 54 | } 55 | 56 | //search the list of existing watchers for an exact match 57 | for( const auto& watch : watchers ){ 58 | if( watch.wd == input.wd ){ 59 | if( watch.path == input.path ){ 60 | spdlog::warn( FMT_STRING( "INOTIFY: already watching: {}" ), path.c_str() ); 61 | } 62 | } 63 | } 64 | //we arent watching the file so add a new entry to the Listing 65 | spdlog::info( FMT_STRING( "INOTIFY: adding path: {}" ), path.parent_path().c_str() ); 66 | watchers.push_back( input ); 67 | return 0; 68 | } 69 | 70 | void 71 | Notifier::check(){ 72 | if( watchers.empty() ){ 73 | spdlog::warn( FMT_STRING( "INOTIFY: nothing to check" ) ); 74 | return;//nothing to check; 75 | } 76 | 77 | int poll_num; 78 | poll_num = poll( &pfd, 1, 0 ); 79 | if( poll_num == -1 ){ 80 | if( errno == EINTR )return; 81 | spdlog::error( FMT_STRING( "INOTIFY: poll" ) ); 82 | return; 83 | } 84 | if( poll_num > 0 ){ 85 | if( pfd.revents & POLLIN ){ 86 | /* Inotify events are available */ 87 | handleEvents(); 88 | } 89 | /* Note: look in git history for example on how to use keyboard input 90 | * from stdin, could be used to have keyboard input to midi events. 91 | */ 92 | } 93 | } 94 | 95 | void 96 | Notifier::handleEvents(){ 97 | /* Some systems cannot read integer variables if they are not 98 | properly aligned. On other systems, incorrect alignment may 99 | decrease performance. Hence, the buffer used for reading from 100 | the inotify file descriptor should have the same alignment as 101 | struct inotify_event. */ 102 | char buf[ 4096 ] __attribute__ ((aligned(__alignof__(struct inotify_event)))); 103 | const struct inotify_event *event; 104 | ssize_t len; 105 | char *ptr; 106 | 107 | /* Loop while events can be read from inotify file descriptor. */ 108 | for(;;){ 109 | /* Read some events. */ 110 | len = read( fd, buf, sizeof buf ); 111 | /* If the nonblocking read() found no events to read, then 112 | it returns -1 with errno set to EAGAIN. In that case, 113 | we exit the loop. */ 114 | if( len == -1 && errno != EAGAIN ){ 115 | spdlog::error( FMT_STRING( "INOTIFY: read" ) ); 116 | return; 117 | } 118 | 119 | if( len <= 0 )break; 120 | 121 | /* Loop over all events in the buffer */ 122 | for( ptr = buf; ptr < buf + len; 123 | ptr += sizeof(struct inotify_event) + event->len ) 124 | { 125 | event = (const struct inotify_event *) ptr; 126 | 127 | //test we are looking at a file we are watching 128 | for( const auto& watcher : watchers ){ 129 | if( watcher.path.filename() == event->name ){ 130 | spdlog::info( FMT_STRING( "INOTIFY: {} has been modified" ), event->name ); 131 | // so run the associated function. 132 | watcher.function(); 133 | return; 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | }//end namespace m2i 141 | -------------------------------------------------------------------------------- /src/uinput.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by enetheru on 22/9/20. 3 | // 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "uinput.h" 13 | 14 | namespace m2i { 15 | Uinput::Uinput(){ 16 | struct libevdev *dev; 17 | dev = libevdev_new(); 18 | libevdev_set_name(dev, "m2i Virtual Input"); 19 | 20 | // We need to unmask the input events we wish to generate 21 | // https://www.kernel.org/doc/html/v4.17/input/event-codes.htm 22 | 23 | //enable relative device events 24 | libevdev_enable_event_type( dev, EV_REL); 25 | for( int axis = 0x00; axis < 0x0a; axis++ ){ 26 | libevdev_enable_event_code( dev, EV_REL, axis, nullptr ); 27 | } 28 | libevdev_enable_event_code( dev, EV_ABS, REL_WHEEL_HI_RES, nullptr ); 29 | libevdev_enable_event_code( dev, EV_ABS, REL_HWHEEL_HI_RES, nullptr ); 30 | 31 | //enable absolute device events 32 | libevdev_enable_event_type( dev, EV_ABS); 33 | for( int axis = 0x00; axis < 0x29; axis++ ){ 34 | libevdev_enable_event_code( dev, EV_ABS, axis, nullptr ); 35 | } 36 | 37 | // enable button and key event types 38 | libevdev_enable_event_type(dev, EV_KEY); 39 | 40 | //enable regular keyboard key codes. 41 | for( int key = 1; key < 249; key++ ){ 42 | libevdev_enable_event_code( dev, EV_KEY, key, nullptr ); 43 | } 44 | 45 | //enable misc buttons 46 | for( int btn = 0x100; btn < 0x10a; btn++ ){ 47 | libevdev_enable_event_code( dev, EV_KEY, btn, nullptr ); 48 | } 49 | 50 | //enable mouse buttons 51 | for( int key = 0x110; key < 0x118; key++ ){ 52 | libevdev_enable_event_code( dev, EV_KEY, key, nullptr ); 53 | } 54 | 55 | //enable joystick buttons 56 | for( int btn = 0x120; btn < 0x130; btn++ ){ 57 | libevdev_enable_event_code( dev, EV_KEY, btn, nullptr ); 58 | } 59 | 60 | // enable gamepad buttons 61 | for( int btn = 0x130; btn < 0x13f; btn++ ){ 62 | libevdev_enable_event_code( dev, EV_KEY, btn, nullptr ); 63 | } 64 | 65 | //enable wheel buttons 66 | for( int btn = 0x150; btn < 0x152; btn++ ){ 67 | libevdev_enable_event_code( dev, EV_KEY, btn, nullptr ); 68 | } 69 | 70 | fd = open("/dev/uinput", O_RDWR); 71 | int rc = libevdev_uinput_create_from_device(dev, fd, &uidev); 72 | 73 | if (rc < 0){ 74 | //no point in continuing if we cannot create a virtual device. 75 | spdlog::critical( FMT_STRING("Failed to create virtual input device: ({})"), strerror(-rc)); 76 | close(fd); 77 | exit(1); 78 | } 79 | 80 | libevdev_free(dev); 81 | } 82 | 83 | Uinput::~Uinput(){ 84 | libevdev_uinput_destroy(uidev); 85 | close(fd); 86 | } 87 | 88 | void Uinput::keypress(int input_event_code) { 89 | keydown( input_event_code ); 90 | keyup( input_event_code ); 91 | } 92 | 93 | void Uinput::keydown(int input_event_code) { 94 | libevdev_uinput_write_event(uidev, EV_KEY, input_event_code, 1); 95 | libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); 96 | } 97 | 98 | void Uinput::keyup(int input_event_code) { 99 | libevdev_uinput_write_event(uidev, EV_KEY, input_event_code, 0); 100 | libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); 101 | } 102 | 103 | void Uinput::mousemove(int rel_x, int rel_y) { 104 | libevdev_uinput_write_event(uidev, EV_REL, REL_X, rel_x); 105 | libevdev_uinput_write_event(uidev, EV_REL, REL_Y, rel_y); 106 | libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); 107 | } 108 | 109 | void Uinput::mousewarp(int abs_x, int abs_y) { 110 | libevdev_uinput_write_event(uidev, EV_ABS, ABS_X, abs_x); 111 | libevdev_uinput_write_event(uidev, EV_ABS, ABS_Y, abs_y); 112 | libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); 113 | } 114 | 115 | void Uinput::mousescroll( int distance ) { 116 | libevdev_uinput_write_event(uidev, EV_REL, REL_WHEEL, distance & -1 ); 117 | libevdev_uinput_write_event(uidev, EV_REL, REL_WHEEL_HI_RES, distance ); 118 | libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); 119 | } 120 | void Uinput::mousehscroll( int distance ) { 121 | libevdev_uinput_write_event(uidev, EV_REL, REL_HWHEEL , distance & -1 ); 122 | libevdev_uinput_write_event(uidev, EV_REL, REL_HWHEEL_HI_RES , distance ); 123 | libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /doc/m2i.1.in: -------------------------------------------------------------------------------- 1 | ./" preview the document using: nroff -e -mandoc 2 | .TH ${DOC_TITLE} 1 ${DOC_DATE} "" "${DOC_NAME} Reference" 3 | .SH NAME 4 | ${DOC_NAME} \- ${DOC_DESCRIPTION_SHORT} 5 | . 6 | .SH SYNOPSIS 7 | .B ${DOC_NAME} 8 | [OPTIONS] 9 | . 10 | .SH DESCRIPTION 11 | ${DOC_DESCRIPTION} 12 | .SH OPTIONS 13 | .TP 14 | .B -h, --help 15 | Print help text and exit 16 | .TP 17 | .B -v, --verbose 18 | Output more information 19 | .TP 20 | .B -q, --quiet 21 | Output less information 22 | .TP 23 | .B -c, --config 24 | Specify the configuration script to use 25 | .TP 26 | .B -s, --script 27 | Specify the script to use 28 | .TP 29 | .B -a, --alsa 30 | Enable ALSA midi sequencer 31 | .TP 32 | .B -j, --jack 33 | Enable jack midi sequencer 34 | . 35 | .SH FILES 36 | ${DOC_NAME} uses a config file and a script file. Examples are available in 37 | ${CMAKE_INSTALL_PREFIX}/share/m2i 38 | .SS $XDG_CONFIG_HOME/m2i/config.lua 39 | Specifies the default configuration, command line options override these values 40 | if given. These values are not guaranteed for the lifetime of the program, as 41 | they are only loaded once on start up, and will disappear if a call to reload is 42 | received from script file monitoring. 43 | .sp 44 | eg. 45 | .nf 46 | .RS 4 47 | config = { 48 | key = value, 49 | ... 50 | } 51 | .RE 52 | .fi 53 | .TP 54 | .B verbose = true|false 55 | More information sent to stdout 56 | .TP 57 | .B quiet = true|false 58 | Less information send to stdout 59 | .TP 60 | .B script = path 61 | Path of the script file to load, if relative will look in current directory, 62 | and then $XDG_CONFIG_HOME/m2i 63 | .TP 64 | .B use_alsa = true|false 65 | Enable the ALSA sequencer back end 66 | .TP 67 | .B use_jack = true|false 68 | Enable the jack sequencer back end 69 | .TP 70 | .B reconnect = true|false 71 | Will attempt to reconnect to jack during the watch loop cycle. This option will 72 | be removed when a better solution is implemented. 73 | .TP 74 | .B loop_enable = true|false 75 | Enables evaluation of the loop() function implemented in the script.lua, and 76 | will be removed once a decent scheduler is implemented. 77 | .TP 78 | .B main_freq = integer 79 | Frequency in milliseconds of the main loop. 80 | .TP 81 | .B loop_freq = integer 82 | Frequency in milliseconds of the loop() function implemented in the script.lua 83 | .TP 84 | .B watch_freq = integer 85 | Frequency in milliseconds of the file monitoring and jack reconnection checks. 86 | .SS $XDG_CONFIG_HOME/m2i/script.lua 87 | The location can be specified on the command line with the -s flag or in the 88 | config.lua as written above 89 | .PP 90 | A minimal example: 91 | .nf 92 | .RS 4 93 | function script_init() 94 | print( "Nothing to init" ) 95 | end 96 | 97 | function midi_recv( status, data1, data2 ) 98 | print( status, data1, data2 ) 99 | end 100 | .RE 101 | .fi 102 | .PP 103 | Look in ${CMAKE_INSTALL_PREFIX}/share/m2i/ for more examples 104 | .PP 105 | The script is watched using inotify and when changed the lua environment is 106 | reset and the script reloaded. This allows you to make changes and not have to 107 | reload m2i manually. 108 | .PP 109 | .SS User defined lua functions: 110 | .TP 111 | .B midi_recv( status, data1, data2 ) 112 | This is the only user defined function that you are required to implement in 113 | your script. midi_recv( ... ) is called for every lua event that is received 114 | by m2i and is where you react to events. 115 | .TP 116 | .B script_init() 117 | Runs once immediately after loading the script. 118 | .TP 119 | .B loop() 120 | Runs at a frequency of loop_freq as specified in the config.lua 121 | .PP 122 | .SS ${DOC_NAME} Inbuilt lua functions: 123 | .br 124 | These functions are implemented by m2i to use in your script. 125 | .TP 126 | .B print( 'string' ... ) 127 | An overload of the lua print functionality to tie it to the main application. 128 | .TP 129 | .B midi_send( { status, data1, data2 } ) 130 | Sends midi events to the output port to connected devices. 131 | .TP 132 | .B loop_enabled() 133 | Enables the loop() function 134 | .TP 135 | .B exec( 'command' ) 136 | Runs an arbitrary command with whatever privileges the m2i utility has, 137 | probably a bad idea really, but so long as its just a user application this is 138 | quite useful. 139 | .TP 140 | .B quit() 141 | Causes the main loop to exit, ending the application 142 | .PP 143 | .SS X11 functions: 144 | .br 145 | These lua functions to interface with the X11 windowing system, for keyboard 146 | and mouse input. 147 | .TP 148 | .B keypress( XK_keycode ) 149 | .TP 150 | .B keydown( XK_keycode ) 151 | .TP 152 | .B keyup( XK_keycode ) 153 | .TP 154 | .B buttonpress( n ) 155 | .TP 156 | .B buttondown( n ) 157 | .TP 158 | .B buttonup( n ) 159 | .TP 160 | .B mousemove( x, y ) 161 | .TP 162 | .B mousepos( x, y ) 163 | .PP 164 | .SS ALSA and Jack functions: 165 | .br 166 | These lua functions to interface with ALSA and Jack sequencers 167 | .TP 168 | .B alsaconnect( 'client', 'port' ) 169 | .TP 170 | .B jackconnect( 'client', 'port' ) 171 | .RE 172 | .PP 173 | .SS Global variables: 174 | .TP 175 | .B WM_CLASS 176 | Is provided for differentiating between applications. It is updated during the 177 | main loop with the contents of the currently focused window's X11 property 178 | WM_CLASS. Using this you can provide a different control mechanism per 179 | application. 180 | .SH AUTHORS 181 | Samuel Nicholas 182 | Urs Liska 183 | Arthur Lutz 184 | Marcin Świgoń 185 | Jarrod Whittaker 186 | Ivan Tkachenko 187 | .SH SEE ALSO 188 | lua(1), jackd(1) 189 | -------------------------------------------------------------------------------- /src/lua.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "lua.h" 4 | #include "midi.h" 5 | #include "util.h" 6 | #include "uinput.h" 7 | 8 | 9 | #ifdef WITH_ALSA 10 | #include "alsa.h" 11 | #endif//WITH_ALSA 12 | 13 | #ifdef WITH_JACK 14 | #include "jack.h" 15 | #endif//WITH_JACK 16 | 17 | #ifdef WITH_XORG 18 | #include "x11.h" 19 | #endif//WITH_XORG 20 | 21 | #include 22 | #include 23 | 24 | namespace m2i { 25 | extern Uinput uinput; 26 | 27 | #ifdef WITH_ALSA 28 | extern AlsaSeq seq; 29 | #endif//WITH_ALSA 30 | 31 | #ifdef WITH_JACK 32 | extern JackSeq jack; 33 | #endif//WITH_JACK 34 | 35 | extern bool quit; 36 | extern bool loop_enabled; 37 | 38 | static const struct luaL_Reg funcs [] = { 39 | {"print", lua_print}, 40 | {"midi_send", lua_midisend}, 41 | {"exec", lua_exec}, 42 | {"quit", lua_quit}, 43 | {"loop_enabled", lua_loopenable}, 44 | {"milliseconds", lua_milliseconds }, 45 | {"keypress", lua_keypress }, 46 | {"keydown", lua_keydown }, 47 | {"keyup", lua_keyup }, 48 | {"mousemove", lua_mousemove }, 49 | {"mousewarp", lua_mousewarp }, 50 | {"mousescroll", lua_mousescroll }, 51 | {"mousehscroll", lua_mousehscroll }, 52 | 53 | #ifdef WITH_XORG 54 | {"detectwindow", lua_detectwindow }, 55 | #endif//WITH_XORG 56 | #ifdef WITH_ALSA 57 | {"alsaconnect", lua_alsaconnect }, 58 | #endif//WITH_ALSA 59 | #ifdef WITH_JACK 60 | {"jackconnect", lua_jackconnect }, 61 | #endif//WITH_JACK 62 | {nullptr, nullptr} /* end of array */ 63 | }; 64 | 65 | lua_State * 66 | register_lua_funcs(lua_State *L ){ 67 | 68 | lua_getglobal(L, "_G"); 69 | luaL_setfuncs(L, funcs, 0); 70 | lua_pop(L, 1); 71 | 72 | return L; 73 | } 74 | 75 | int 76 | midi_to_lua(lua_State *L, const midi_event &event ) 77 | { 78 | lua_getglobal( L, "midi_recv" ); 79 | lua_pushnumber( L, event.status ); 80 | lua_pushnumber( L, event.data1 ); 81 | lua_pushnumber( L, event.data2 ); 82 | if( lua_pcall( L, 3, 0, 0 ) != LUA_OK ) { 83 | spdlog::error(FMT_STRING("LUA: call to function 'midi_recv' failed {}"), lua_tostring(L, -1)); 84 | lua_pop( L, 1 ); 85 | } 86 | return 0; 87 | } 88 | 89 | /* ========================= Lua Functions =========================== */ 90 | int 91 | lua_print( lua_State* L ){ 92 | std::stringstream output; 93 | 94 | int args = lua_gettop( L ); 95 | for( int i = 1; i <= args; ++i ){ 96 | output << lua_tostring(L, i); 97 | } 98 | spdlog::info( FMT_STRING( "SCRIPT: {}" ), output.str() ); 99 | return 0; 100 | } 101 | 102 | int 103 | lua_midisend( lua_State *L ) 104 | { 105 | midi_event event{}; 106 | lua_pushnumber( L, 1 ); 107 | lua_gettable( L, -2 ); 108 | event.status = static_cast( luaL_checknumber( L, -1 ) ); 109 | lua_pop( L, 1 ); 110 | 111 | lua_pushnumber( L, 2 ); 112 | lua_gettable( L, -2 ); 113 | event.data1 = static_cast( luaL_checknumber( L, -1 ) ); 114 | lua_pop( L, 1 ); 115 | 116 | lua_pushnumber( L, 3 ); 117 | lua_gettable( L, -2 ); 118 | event.data2 = static_cast( luaL_checknumber( L, -1 ) ); 119 | lua_pop( L, 1 ); 120 | 121 | #ifdef WITH_ALSA 122 | if( m2i::seq )m2i::seq.event_send( event ); 123 | #endif 124 | 125 | #ifdef WITH_JACK 126 | if( m2i::jack.valid )m2i::jack.event_send( event ); 127 | #endif 128 | return 0; 129 | } 130 | 131 | int 132 | lua_exec( lua_State *L ) 133 | { 134 | std::string command; 135 | command = luaL_checkstring( L, -1 ); 136 | spdlog::info( FMT_STRING( "LUA: exec: {}" ), command ); 137 | 138 | FILE *in; 139 | char buff[512]; 140 | if(! (in = popen( command.c_str(), "r" ))){ 141 | return 1; 142 | } 143 | while( fgets(buff, sizeof(buff), in) != nullptr ){ 144 | // strip trailling newline 145 | size_t bufflen = strnlen(buff, 512); 146 | if( bufflen > 0 && buff[bufflen - 1] == '\n' ){ 147 | buff[bufflen - 1] = '\0'; 148 | } 149 | 150 | spdlog::info( FMT_STRING( "LUA: {}" ), buff ); 151 | } 152 | pclose( in ); 153 | return 0; 154 | } 155 | 156 | int 157 | lua_quit( lua_State *L ) 158 | { 159 | (void)L; //shutup unused variable; 160 | m2i::quit = true; 161 | return 0; 162 | } 163 | 164 | int 165 | lua_loopenable( lua_State *L ) 166 | { 167 | (void)L; //shutup unused variable; 168 | m2i::loop_enabled = true; 169 | return 0; 170 | } 171 | 172 | int 173 | lua_milliseconds( lua_State *L ) 174 | { 175 | static auto first = std::chrono::system_clock::now(); 176 | auto now = std::chrono::system_clock::now(); 177 | auto duration = std::chrono::duration_cast(now - first).count(); 178 | lua_pushnumber( L, duration ); 179 | return 1; 180 | } 181 | 182 | /* ==================== sending events to uinput ====================*/ 183 | int 184 | lua_keypress( lua_State *L ) 185 | { 186 | auto input_event_code = static_cast( luaL_checknumber( L, 1 ) ); 187 | uinput.keypress( input_event_code ); 188 | spdlog::info( FMT_STRING( "LUA: keypress: {}" ), input_event_code ); 189 | return 0; 190 | } 191 | 192 | int 193 | lua_keydown( lua_State *L ) 194 | { 195 | auto input_event_code = static_cast( luaL_checknumber( L, 1 ) ); 196 | uinput.keydown( input_event_code ); 197 | spdlog::info( FMT_STRING( "LUA: keydown: {}" ), input_event_code ); 198 | return 0; 199 | } 200 | 201 | int 202 | lua_keyup( lua_State *L ) 203 | { 204 | auto input_event_code = static_cast( luaL_checknumber( L, 1 ) ); 205 | uinput.keyup( input_event_code ); 206 | spdlog::info( FMT_STRING( "LUA: keyup: {}" ), input_event_code ); 207 | return 0; 208 | } 209 | 210 | int 211 | lua_mousemove( lua_State *L ) 212 | { 213 | auto x = static_cast( luaL_checknumber( L, 1 ) ); 214 | auto y = static_cast( luaL_checknumber( L, 2 ) ); 215 | uinput.mousemove( x, y ); 216 | 217 | spdlog::info( FMT_STRING( "LUA: mousemove: {},{}" ), x, y ); 218 | return 0; 219 | } 220 | 221 | int 222 | lua_mousewarp(lua_State *L ) 223 | { 224 | auto x = static_cast( luaL_checknumber( L, 1 ) ); 225 | auto y = static_cast( luaL_checknumber( L, 2 ) ); 226 | uinput.mousewarp( x,y ); 227 | 228 | spdlog::info( FMT_STRING( "LUA: mousewarp: {},{}" ), x, y ); 229 | return 0; 230 | } 231 | 232 | int 233 | lua_mousescroll( lua_State *L ) 234 | { 235 | auto distance = static_cast( luaL_checknumber( L, 1 ) ); 236 | uinput.mousescroll( distance ); 237 | spdlog::info( FMT_STRING( "LUA: mousescroll: {}" ), distance ); 238 | return 0; 239 | } 240 | 241 | int 242 | lua_mousehscroll( lua_State *L ) 243 | { 244 | auto distance = static_cast( luaL_checknumber( L, 1 ) ); 245 | uinput.mousehscroll( distance ); 246 | spdlog::info( FMT_STRING( "LUA: mousehscroll: {}" ), distance ); 247 | return 0; 248 | } 249 | 250 | #ifdef WITH_XORG /* ===================== X11 functions =========================== */ 251 | int 252 | lua_detectwindow( lua_State *L ) 253 | { 254 | Display *xdp = XOpenDisplay( getenv( "DISPLAY" ) ); 255 | std::string windowname = m2i::XDetectWindow( xdp ); 256 | XCloseDisplay( xdp ); 257 | 258 | if( windowname.empty() ) 259 | return 0; 260 | lua_pushstring( L , windowname.c_str() ); 261 | lua_setglobal( L, "WM_CLASS" ); 262 | return 0; 263 | } 264 | #endif//WITHXORG 265 | 266 | #ifdef WITH_ALSA 267 | /* ==================== ALSA Lua Bindings =========================== */ 268 | int 269 | lua_alsaconnect( lua_State *L ) 270 | { 271 | /* the idea with this function is that it takes two named ports, or port id's 272 | * and connects them together, so the function takes two arguments, client 273 | * and port */ 274 | std::string client = luaL_checkstring(L, 1); 275 | std::string port = luaL_checkstring(L, 2); 276 | m2i::seq.connect( client, port ); 277 | return 0; 278 | } 279 | #endif//WITH_ALSA 280 | 281 | #ifdef WITH_JACK 282 | /* ==================== Jack Lua Bindings =========================== */ 283 | int 284 | lua_jackconnect( lua_State *L ) 285 | { 286 | (void)L; 287 | //TODO connect to jack port 288 | spdlog::warn( FMT_STRING( "LUA: This function is not implemented yet" ) ); 289 | return 0; 290 | } 291 | #endif//WITH_JACK 292 | }// end namespace m2i 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![m2i](https://gitlab.com/enetheru/midi2input/raw/master/res/icons/m2i-black.png "m2i") 2 | ======= 3 | m2i(midi to input) is a small service like application that runs scripted 4 | actions in response to to midi events. Actions can be mouse, keyboard events, 5 | commands and more midi events. m2i can receive midi events from either the ALSA 6 | and or Jack midi sequencer. 7 | 8 | m2i dependencies 9 | ---------------- 10 | * Linux 11 | * [argh](https://github.com/adishavit/argh) - command line processor 12 | * [libfmt](https://github.com/fmtlib/fmt) - simple formatting 13 | * [spdlog](https://github.com/gabime/spdlog) - fast logging 14 | * lua 5.3 - the script engine 15 | * libx11 - for X11 window WM_CLASS detection 16 | * either alsa-lib or jack/jack2 - for midi sequencer input 17 | * qt5-base - for system tray icon 18 | 19 | Installation Instructions 20 | ========================= 21 | Check INSTALL[.OS].md for build and install instructions 22 | 23 | Features 24 | ======== 25 | * lua script reactions to midi events 26 | * sending of midi events 27 | * routing via ALSA or jack using standard tools 28 | * sending keyboard and mouse events from scripts 29 | * executing commands from scripts 30 | * per application control by switching on WM_CLASS attribute 31 | * Qt5 based system tray icon 32 | * detection and reloading of changed script file 33 | * reconnect to device after unplug/plug 34 | 35 | Running 36 | ======= 37 | 38 | ``` 39 | m2i --help 40 | m2i --config cfg/config --script cfg/basic.lua 41 | m2i --config cfg/config --script cfg/basic.lua --verbose 42 | ``` 43 | 44 | If you are setting up a new device and using ALSA, you have to connect the MIDI 45 | device to ALSA : 46 | 47 | * get the device name with 48 | 49 | ``` 50 | aconnect -l 51 | client 24: 'ProductName' [type=kernel,card=2] 52 | 0 'ProductName MIDI 1' 53 | client 128: 'midi2input_alsa' [type=user,pid=22962] 54 | 0 'in ' 55 | 1 'out ' 56 | 57 | ``` 58 | 59 | * connect it 60 | 61 | ``` 62 | aconnect 'ProductName' 'midi2input_alsa' 63 | aconnect 'midi2input_alsa':1 'ProductName' 64 | 65 | ``` 66 | 67 | You should now see `ALSA: Port Subscription from: 'ProductName':'ProductName MIDI 1'` 68 | in the logs. 69 | 70 | Why? 71 | ==== 72 | Because I had a midi controller, and I thought it was a shame that I could not 73 | control my pc using it. I did a little research online and it didn't seem that 74 | there was an existing solution that fit my needs, indeed I didn't find a 75 | solution at all that was FLOSS. 76 | 77 | | Intro Vid | 78 | | --------- | 79 | | [![Intro Vid](https://img.youtube.com/vi/wr1AqlDXnYI/0.jpg)](http://www.youtube.com/watch?v=wr1AqlDXnYI) | 80 | 81 | | Adding a new Mapping | 82 | | -------------------- | 83 | | [![Adding a new mapping](https://i9.ytimg.com/vi/T_s2V2JzHwo/mq3.jpg?sqp=CJSb5ekF&rs=AOn4CLAi1M4P4ZJUNhatsiuYcRG60_Qakw)](https://www.youtube.com/embed/T_s2V2JzHwo) | 84 | 85 | Examples 86 | ======== 87 | * Using my midi controller play/pause, jogwheel, volume, tempo buttons to 88 | control media player 89 | * Using the jogwheel and buttons to have a custom controller for video editing 90 | software 91 | * A sound and video board and controlling cameras for video streaming 92 | * Jogwheel as mouse scroll 93 | * Shop demo for turning on and off lights 94 | * Turn a keboard/piano into a puzzle for escape rooms 95 | 96 | Man page dump 97 | ============= 98 | 99 | ``` 100 | M2I(1) m2i Reference M2I(1) 101 | 102 | NAME 103 | m2i - midi input -> keyboard|mouse|midi|command output 104 | 105 | SYNOPSIS 106 | m2i [OPTIONS] 107 | 108 | DESCRIPTION 109 | m2i(midi to input) is a small service like applicatin that runs 110 | scripted actions in response to to midi events. Actions can be mouse, 111 | keyboard events, commands and more midi events. m2i can receive midi 112 | events from either the ALSA and or Jack midi sequenser. 113 | 114 | OPTIONS 115 | -h, --help 116 | Print help text and exit 117 | 118 | -v, --verbose 119 | Output more information 120 | 121 | -q, --quiet 122 | Output less information 123 | 124 | -c, --config 125 | Specify the configuration script to use 126 | 127 | -s, --script 128 | Specify the script to use 129 | 130 | -a, --alsa 131 | Enable ALSA midi sequencer 132 | 133 | -j, --jack 134 | Enable jack midi sequencer 135 | 136 | FILES 137 | m2i uses a config file and a script file. Examples are available in 138 | /usr/share/m2i 139 | 140 | $XDG_CONFIG_HOME/m2i/config.lua 141 | Specifies the default configuration, command line options override 142 | these values if given. These values are not guaranteed for the life‐ 143 | time of the program, as they are only loaded once on start up, and 144 | will disappear if a call to reload is received from script file moni‐ 145 | toring. 146 | 147 | eg. 148 | config = { 149 | key = value, 150 | ... 151 | } 152 | 153 | verbose = true|false 154 | More information sent to stdout 155 | 156 | quiet = true|false 157 | Less information send to stdout 158 | 159 | script = path 160 | Path of the script file to load, if relative will look in cur‐ 161 | rent directory, and then $XDG_CONFIG_HOME/m2i 162 | 163 | use_alsa = true|false 164 | Enable the ALSA sequencer back end 165 | 166 | use_jack = true|false 167 | Enable the jack sequencer back end 168 | 169 | reconnect = true|false 170 | Will attempt to reconnect to jack during the watch loop cycle. 171 | This option will be removed when a better solution is imple‐ 172 | mented. 173 | 174 | loop_enable = true|false 175 | Enables evaluation of the loop() function implemented in the 176 | script.lua, and will be removed once a decent scheduler is im‐ 177 | plemented. 178 | 179 | main_freq = integer 180 | Frequency in milliseconds of the main loop. 181 | 182 | loop_freq = integer 183 | Frequency in milliseconds of the loop() function implemented in 184 | the script.lua 185 | 186 | watch_freq = integer 187 | Frequency in milliseconds of the file monitoring and jack re‐ 188 | connection checks. 189 | 190 | $XDG_CONFIG_HOME/m2i/script.lua 191 | The location can be specified on the command line with the -s flag or 192 | in the config.lua as written above 193 | 194 | A minimal example: 195 | function script_init() 196 | print( "Nothing to init" ) 197 | end 198 | 199 | function midi_recv( status, data1, data2 ) 200 | print( status, data1, data2 ) 201 | end 202 | 203 | Look in /usr/share/m2i/ for more examples 204 | 205 | The script is watched using inotify and when changed the lua environ‐ 206 | ment is reset and the script reloaded. This allows you to make changes 207 | and not have to reload m2i manually. 208 | 209 | User defined lua functions: 210 | midi_recv( status, data1, data2 ) 211 | This is the only user defined function that you are required to 212 | implement in your script. midi_recv( ... ) is called for every 213 | lua event that is received by m2i and is where you react to 214 | events. 215 | 216 | script_init() 217 | Runs once immediately after loading the script. 218 | 219 | loop() Runs at a frequency of loop_freq as specified in the config.lua 220 | 221 | m2i Inbuilt lua functions: 222 | These functions are implemented by m2i to use in your script. 223 | 224 | print( 'string' ... ) 225 | An overload of the lua print functionality to tie it to the 226 | main application. 227 | 228 | midi_send( { status, data1, data2 } ) 229 | Sends midi events to the output port to connected devices. 230 | 231 | loop_enabled() 232 | Enables the loop() function 233 | 234 | exec( 'command' ) 235 | Runs an arbitrary command with whatever privileges the m2i 236 | utility has, probably a bad idea really, but so long as its 237 | just a user application this is quite useful. 238 | 239 | quit() Causes the main loop to exit, ending the application 240 | 241 | X11 functions: 242 | These lua functions to interface with the X11 windowing system, for 243 | keyboard and mouse input. 244 | 245 | keypress( XK_keycode ) 246 | 247 | keydown( XK_keycode ) 248 | 249 | keyup( XK_keycode ) 250 | 251 | buttonpress( n ) 252 | 253 | buttondown( n ) 254 | 255 | buttonup( n ) 256 | 257 | mousemove( x, y ) 258 | 259 | mousepos( x, y ) 260 | 261 | ALSA and Jack functions: 262 | These lua functions to interface with ALSA and Jack sequencers 263 | 264 | alsaconnect( 'client', 'port' ) 265 | 266 | jackconnect( 'client', 'port' ) 267 | 268 | Global variables: 269 | WM_CLASS 270 | Is provided for differentiating between applications. It is up‐ 271 | dated during the main loop with the contents of the currently 272 | focused window's X11 property WM_CLASS. Using this you can pro‐ 273 | vide a different control mechanism per application. 274 | 275 | AUTHORS 276 | Samuel Nicholas 277 | Urs Liska 278 | Arthur Lutz 279 | Marcin Świgoń 280 | Jarrod Whittaker 281 | Ivan Tkachenko 282 | 283 | SEE ALSO 284 | lua(1), jackd(1) 285 | 286 | 2020-09-16 M2I(1) 287 | ``` 288 | -------------------------------------------------------------------------------- /Diary.md: -------------------------------------------------------------------------------- 1 | Enetheru: Sometime back in the day 2 | ------------------------ 3 | so on inspection of wanting to update the documentation i have come to the 4 | conclusion that: 5 | * README.md - is primarily an advertisement and depth of description and not used for 6 | reference 7 | * man page - is the primary reference inforamtion and it might be possible to manually update the README.md by tacking on the man page at the bottom. 8 | 9 | so README.md = advertisement + deep description + man page 10 | that way it becomes easier to manage. 11 | 12 | so an idea i had which was to convert the man page to markdown and tack it onto 13 | the end of README.md turned out to be harder than i expected. 14 | 15 | it appears as though nobody is really converting man to markdown, they do it 16 | the other way which IMHO seems weird.. anyway. 17 | 18 | I needed to get http://www.kylheku.com/cgit/man/ 19 | so that i had a better man2html converter, beacuse the one that comes with arch 20 | is shit. 21 | 22 | then i used pandoc to convert that to markdown and i still have to clean it up. 23 | the commands are thus: 24 | * build project first 25 | * man2html m2i.1 > temp.html 26 | * pandoc -f html -t markdown temp.html temp.md 27 | 28 | or all in one go 29 | 30 | cmake ../ && ~/bin/man2html < m2i.1 > test.html && pandoc -f html -t markdown test.html > test.md 31 | 32 | then manually edit.. its notas good as automatic but it isnt totally shit. 33 | 34 | this helps: 35 | https://liw.fi/manpages/ 36 | 37 | Enetheru: Tue 15 Sep 2020 12:24:22 38 | ----------------------------- 39 | Looks like this project is pre-diary, which is interesting. All newer things I 40 | create have a Diary to use as a thought dump. 41 | 42 | Anyway, Today I decided to take a look at where this stands, I've had a few 43 | interested parties over the years, but they as expected come and go, I would 44 | like perhaps to make this friendlier to updating with features that make sense. 45 | 46 | But mainly I wish to perform some maintenance more than anything, it's been a 47 | while and that can be informative. 48 | 49 | Enetheru: Sun 20 Sep 2020 16:02:29 50 | ---------------------------------- 51 | Whilst reviewing some of the code today to try to clean up some items, create 52 | QT menu's and such I have started thinking about how the various components 53 | need a consistent interface so that interacting with it via LUA, QT, DBus, 54 | c++, and anything else that comes along will be consistent. 55 | 56 | I would expect that these things bundled into a sort of library as a core 57 | component, and then shared among the optional interfaces would be the way to 58 | go. 59 | 60 | It becomes more and more like ingesting a thing into the lua and then spitting 61 | it back out after modification just with a focus on midi. And as such makes me 62 | think that there is probably a lower level subsystem that does this job already 63 | that I am simply unaware of. 64 | 65 | Which sends me down one of the other goals for the project, to write a 66 | uinput/evdev driver so that it can work regardless of xorg. True midi2input 67 | conversion. 68 | 69 | [Linux Input Subsystem Userspace API](https://www.kernel.org/doc/html/v4.12/input/input.html) 70 | 71 | It's always a cascade of todo's till you hit bedrock and refactor the whole 72 | project lol. 73 | 74 | So far I've at least identified some things that need to change. 75 | * libify the main app 76 | * provide consistent interface 77 | * turn to uinput to really simplify/push the project forward. 78 | 79 | There dont appear to be too many examples of uinput/libevdev that are 80 | immediately obvious, but I bet there are a ton of real world code using them. 81 | 82 | Really if we can get uinput/evdev input working then we can nix the x11 83 | dependency and make it viable for headless systems 84 | 85 | Been reading and reading, good old Who-T an Adelaide born fella, for coming 86 | through with the goods again.. 87 | [creating an uinput device](https://who-t.blogspot.com/2013/09/libevdev-creating-uinput-devices.html) 88 | 89 | Looks like Who-T has convinced me to use libevdev even though the freedesktop 90 | site is broken and wont let me register. He has a synced fork though so should 91 | be OK. 92 | 93 | His blog is a treasure trove of input related things, I remember back in the 94 | day following along his work on multi seat setups, thought collab on blender 95 | could have been epic. 96 | 97 | Enetheru: Mon 21 Sep 2020 14:34:03 98 | ---------------------------------- 99 | it's entirely possible that after implementing the midi2input evdev translation 100 | layer, that it could be used for all devices immediately as midi is generic. 101 | 102 | It might be nice to massage the data a little in some cases, but it appears to 103 | be possible to do a straight forward thing. 104 | 105 | Then all the reactionary things can be performed like all the other input 106 | things like volume keys, and power buttons, laptop lid switch etc. 107 | 108 | Certainly makes me think on how I want to push this project forward. Seems 109 | simpler and more complicated at the same time. 110 | 111 | Enetheru: Tue 22 Sep 2020 10:32:48 112 | ---------------------------------- 113 | So continuing down this uinput thread, when creating a new input device from 114 | scratch you need to enable the codes individually to unmask them and allow the 115 | kernel to send through the codes. 116 | 117 | Firstly by event type `libevdev_enable_event_type(dev, type );` 118 | And then by event code `libevdev_enable_event_code(dev, type, code, data);` 119 | 120 | This makes me think that there needs to be a configuration file to firstly 121 | enable these things on creation. I wonder what the consequences of enabling 122 | everything would be. 123 | 124 | This is useful 125 | [Difference between uinput and evdev](https://who-t.blogspot.com/2016/05/the-difference-between-uinput-and-evdev.html) 126 | [Understanding evdev](https://who-t.blogspot.com/2016/09/understanding-evdev.html) 127 | [input event code descriptions](https://www.kernel.org/doc/html/v4.12/input/event-codes.html) 128 | 129 | The thing I'm stuck on is what the convention is for generating events, and 130 | whether that will provide the outcomes I want. 131 | Because evdev events can be very interesting as they are composed of multiple 132 | event/code tuples with optional data, before a SYN synchronisation frame. 133 | 134 | This means that its possible to compose up some fairly complicated things if 135 | desired, but that doesn't mean that it will be useful, so the standard 136 | convention might be to copy whatever my current keyboard does. 137 | 138 | Another thing that bugs me is that there isn't any description of arbitrary 139 | codes anywhere, most of what is described shows standard mouse/keyboard 140 | hardware, touchpad(because that's relatively new) and joysticks. nothing in 141 | there about completely custom random buttons that don't have a standardisation. 142 | 143 | And would that even show up in window managers as a thing to map to hotkey 144 | combinations for software? 145 | 146 | I'd rather not fudge things if I can help it, its important to me to put some 147 | effort into getting it right over expediency. 148 | 149 | As I understand things currently: 150 | * Configuration file to unmask evdev event and code bits 151 | * mapping to transcode midi events to evdev emissions 152 | 153 | I suppose the expedient thing is to just replace all xtst code with libevdev 154 | code. 155 | 156 | It would still be nice to detect the current application and switch layouts 157 | based on detection, because its not like a keyboard. So some X thing will need 158 | to stay for now. 159 | 160 | A thread on Stack overflow describing the problem of detecting active 161 | window with links to various projects attempting to implement this feature 162 | [Stack Overflow Thread](https://stackoverflow.com/questions/45465016/how-do-i-get-the-active-window-on-gnome-wayland) 163 | 164 | Lets see, the naive approach would be to simply emit keyboard events, then at 165 | least the buttons can be mapped to application functionality. 166 | 167 | Given I cannot re-implement focused application re-mapping then perhaps that 168 | could be something for the qt menu to switch scripts, or have options. 169 | 170 | Hmm, perhaps it would be more wise to copy the keyboard rather than create a 171 | new one. 172 | 173 | Enetheru: Tue 13 Oct 2020 12:47:07 174 | ---------------------------------- 175 | It appears that when udev creates the device nodes in /dev it pattern matches 176 | against the enabled bits for input devices, which means that symlinks will be 177 | mutually exclusive, and in order to emulate different types of hardware, it 178 | will be necessary to have multiple evdev devices. 179 | 180 | This pushes one more towards requiring configuration that will enable types of 181 | hardware, and then maps midi events to those specific types. 182 | 183 | The input-event-codes.h is laid out with sections of various input devices 184 | and their corresponding input codes. 185 | 186 | I imagine that within the lua configuration sections would need to be added to 187 | enable a type of device mapping, and then what midi events to use as the 188 | mapping. 189 | 190 | An alternative would be to require multiple instances of m2i, but for a general 191 | use case I don't see this as being practical. For sound engineers it might be 192 | useful however different configuration per m2i instance, attached to separate 193 | midi devices. So the trade off would be multiple configurations with event 194 | routing from different midi devices within the same executable, or separate 195 | processes. Trade-offs. 196 | 197 | I think the simple case right now would be to implement it as simple as 198 | possible as a replacement for the xtst code, the only question is whether 199 | keyboard and mouse events are still generated if udev detects the device as a 200 | joystick. and for that I need to be at my desk, not at a cafe. 201 | 202 | Enetheru: Tue 13 Oct 2020 17:21:39 203 | ---------------------------------- 204 | So far so good, it appears that it doesn't matter if I enable everything, for me 205 | at least the scroll actions happen even thought there isn't a mouse device. 206 | 207 | Lets keep going. 208 | 209 | So I have keypresses working, and scroll events. does it show up in gamepad 210 | programs? yup, tested with qjoypad for translating gamepad events to 211 | keypresses. 212 | 213 | This basically allows us to do straight translation from midi directly to input 214 | of any kind. Now how to make nice. 215 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | //system includes 2 | #include 3 | #include 4 | #include 5 | #include 6 | namespace fs = std::filesystem; 7 | 8 | //library includes 9 | extern "C" { 10 | #include 11 | #include 12 | #include 13 | } 14 | #include "argh.h" 15 | #include 16 | 17 | //local includes 18 | #include "util.h" 19 | #include "midi.h" 20 | #include "inotify.h" 21 | #include "uinput.h" 22 | #include "lua.h" 23 | 24 | #ifdef WITH_JACK 25 | #include "jack.h" 26 | #endif//WITH_JACK 27 | 28 | #ifdef WITH_ALSA 29 | #include "alsa.h" 30 | #endif//WITH_ALSA 31 | 32 | #ifdef WITH_QT 33 | #include 34 | #include 35 | #include "trayicon.h" 36 | #endif//WITH_QT 37 | 38 | #ifdef WITH_XORG 39 | #include "x11.h" 40 | #endif//WITH_XORG 41 | 42 | namespace m2i { 43 | const char *helptext = 44 | "USAGE: ./m2i [options]\n" 45 | "OPTIONS:\n" 46 | " -h --help Print usage and exit\n" 47 | " -v --verbose Output more information\n" 48 | " -q --quiet Output less information\n" 49 | " -c --config Specify config file\n" 50 | " -s --script Specify script file\n" 51 | " -a --alsa Use ALSA midi backend\n" 52 | " -j --jack Use Jack midi backend\n"; 53 | //decided that unless it seems paramount, then further configuration should go 54 | //into the config file 55 | 56 | /* ====================== Global Options ============================ */ 57 | // Yuck i know haha.. oh well. 58 | int loglevel = 100; 59 | bool use_alsa = true; 60 | bool use_jack = false; 61 | bool reconnect = true; 62 | bool loop_enabled = true; 63 | std::chrono::milliseconds main_freq( 10 ); 64 | std::chrono::milliseconds loop_freq( 100 ); 65 | std::chrono::milliseconds watch_freq( 1000 ); 66 | fs::path config = "config.lua"; 67 | fs::path script = "script.lua"; 68 | 69 | //program state 70 | lua_State *L = nullptr; 71 | Notifier notifier; 72 | bool quit = false; 73 | 74 | #ifdef WITH_ALSA 75 | AlsaSeq seq; 76 | #endif//WITH_ALSA 77 | 78 | #ifdef WITH_JACK 79 | JackSeq jack; 80 | #endif//WITH_JACK 81 | 82 | //uinput driver 83 | m2i::Uinput uinput; 84 | 85 | }//end namespace m2i 86 | 87 | //signal interrupt handler for ctrl+c 88 | static void 89 | intHandler( int dummy ){ 90 | (void)dummy; 91 | m2i::quit = true; 92 | } 93 | 94 | static void 95 | loadConfig( lua_State *L, const fs::path &path ){ 96 | // Load configuraton lua script 97 | spdlog::info( FMT_STRING( "LUA: Loading configuration: {}" ), path.c_str() ); 98 | if( luaL_loadfile( L, path.c_str() ) || lua_pcall( L, 0, 0, 0 ) ){ 99 | spdlog::error( FMT_STRING( "LUA: failure loading configuration file: {}" ), lua_tostring( L, -1 ) ); 100 | lua_pop( L, 1 ); 101 | return; 102 | } 103 | 104 | //pull configuration from config 105 | lua_getglobal( L, "config" ); 106 | if( !lua_istable(L, -1 ) ){ 107 | spdlog::error( FMT_STRING( "LUA: No 'config' table found in {}" ), path.c_str() ); 108 | lua_pop( L, 1 ); 109 | return; 110 | } 111 | 112 | // Collect unused properties into vector, to warn about them later. 113 | // This is done in batch to ensure that we honor loglevel which possibly mutes warnings. 114 | std::vector unrecognized_vars{}; 115 | 116 | lua_pushnil( L ); 117 | while( lua_next( L, -2 ) != 0 ){ 118 | std::string var( lua_tostring( L, -2 ) ); 119 | if( var == "script" )m2i::script = lua_tostring( L, -1 ) ; 120 | else if( var == "loglevel" ){ 121 | //Check spdlog/common.h:125 for numbers and labels 122 | spdlog::set_level( static_cast(lua_tointeger( L, -1 )) ); 123 | } 124 | else if( var == "use_alsa" )m2i::use_alsa = lua_toboolean( L, -1 ); 125 | else if( var == "use_jack" )m2i::use_jack = lua_toboolean( L, -1 ); 126 | else if( var == "reconnect" )m2i::reconnect = lua_toboolean( L, -1 ); 127 | else if( var == "loop_enabled" )m2i::loop_enabled = lua_toboolean( L, -1 ); 128 | else if( var == "main_freq" )m2i::main_freq = std::chrono::milliseconds( lua_tointeger( L, -1 ) ); 129 | else if( var == "loop_freq" )m2i::loop_freq = std::chrono::milliseconds( lua_tointeger( L, -1 ) ); 130 | else if( var == "watch_freq" )m2i::watch_freq = std::chrono::milliseconds( lua_tointeger( L, -1 ) ); 131 | else unrecognized_vars.push_back( std::move( var ) ); 132 | lua_pop( L, 1 ); 133 | } 134 | lua_pop( L, 1 ); //lua_getglobal "config" 135 | 136 | for( const std::string &var : unrecognized_vars ){ 137 | spdlog::warn( FMT_STRING( "CONFIG: Unrecognized configuration key: {}" ), var ); 138 | } 139 | } 140 | 141 | static void 142 | inotify_script_change() 143 | { 144 | spdlog::info( FMT_STRING( "restarting lua" ) ); 145 | //blow away the lua state 146 | if( m2i::L )lua_close( m2i::L ); 147 | m2i::L = nullptr; 148 | 149 | //start from scratch 150 | m2i::L = luaL_newstate(); 151 | luaL_openlibs( m2i::L ); 152 | 153 | //register out functions with lua 154 | m2i::register_lua_funcs(m2i::L); 155 | 156 | spdlog::info( FMT_STRING( "LUA: Loading script: {}" ), m2i::script.c_str() ); 157 | if( luaL_loadfile( m2i::L, m2i::script.c_str() ) || lua_pcall( m2i::L, 0, 0, 0 ) ){ 158 | spdlog::error( FMT_STRING( "LUA: failure loading script file: {}" ), lua_tostring( m2i::L, -1 ) ); 159 | lua_pop( m2i::L, 1); 160 | return; 161 | } 162 | 163 | lua_getglobal( m2i::L, "script_init" ); 164 | if( lua_pcall( m2i::L, 0, 0, 0 ) != LUA_OK ){ 165 | spdlog::warn( FMT_STRING("LUA: Missing function: {}"), lua_tostring( m2i::L, -1) ); 166 | lua_pop( m2i::L, 1); 167 | } 168 | } 169 | 170 | int 171 | main( int argc, char **argv ) 172 | { 173 | //handle ctrl+c to exit the main loop. 174 | signal(SIGINT, intHandler); 175 | 176 | /* ============================ Argh Parser ========================= */ 177 | //add key/value pairs 178 | argh::parser cmdl( { "-c", "--config", "-s", "--script" } ); 179 | cmdl.parse( argc, argv, argh::parser::PREFER_PARAM_FOR_UNREG_OPTION ); 180 | 181 | if( cmdl[{"-h", "--help" }] ){ 182 | fmt::print( m2i::helptext ); 183 | exit( 0 ); 184 | } 185 | if( cmdl[{"-v", "--verbose"}] )m2i::loglevel = 100; 186 | if( cmdl[{"-q", "--quiet" }] )m2i::loglevel = 1; 187 | if( cmdl({"-c", "--config" }) ) 188 | m2i::config = m2i::getPath( cmdl({"-c", "--config"}).str() ); 189 | else 190 | m2i::config = m2i::getPath( "config.lua" ); 191 | 192 | /* =================== Lua Initialisation ================= */ 193 | m2i::L = luaL_newstate(); 194 | luaL_openlibs( m2i::L ); 195 | m2i::register_lua_funcs(m2i::L); 196 | 197 | /* === Load Configuration === */ 198 | // We only do this once on program load, reloading the script does not reload configuration items. 199 | loadConfig( m2i::L, m2i::config ); 200 | 201 | //command line overrides 202 | if( cmdl[{"-a", "--alsa" }] )m2i::use_alsa = true; 203 | if( cmdl[{"--no-alsa" }] )m2i::use_alsa = false; 204 | if( cmdl[{"-j", "--jack" }] )m2i::use_jack = true; 205 | if( cmdl[{"--no-jack" }] )m2i::use_jack = false; 206 | if( cmdl({"-s", "--script" }) )m2i::script = cmdl({"-s", "--script"}).str(); 207 | m2i::script = m2i::getPath( m2i::script ); 208 | 209 | //check that we at least use one midi backend, otherwise there is kinda no point 210 | if( !m2i::use_alsa && !m2i::use_jack ){ 211 | spdlog::error( FMT_STRING( "neither jack nor alsa has been specified" ) ); 212 | exit(-1); 213 | } 214 | 215 | /* ============================== ALSA ============================== */ 216 | if( m2i::use_alsa ){ 217 | #ifdef WITH_ALSA 218 | m2i::seq.open(); 219 | #else 220 | spdlog::error( FMT_STRING( "Not compiled with ALSA midi backend" ) ); 221 | exit(-1); 222 | #endif 223 | } 224 | 225 | /* ============================== Jack ============================== */ 226 | if( m2i::use_jack ){ 227 | #ifdef WITH_JACK 228 | m2i::jack.init(); 229 | #else 230 | spdlog::error( FMT_STRING( "Not compiled with Jack midi backend" ) ); 231 | exit(-1); 232 | #endif 233 | } 234 | 235 | /* ============================= X11 ================================ */ 236 | #ifdef WITH_XORG 237 | Display *xdp; 238 | if(! (xdp = XOpenDisplay( getenv( "DISPLAY" ) )) ){ 239 | spdlog::error( FMT_STRING( "Unable to open X display" ) ); 240 | exit( -1 ); 241 | } 242 | XCloseDisplay( xdp ); 243 | XSetErrorHandler( m2i::XErrorCatcher ); 244 | #endif 245 | 246 | /* ======================== QT System Tray ========================== */ 247 | #ifdef WITH_QT 248 | QApplication app(argc, argv); 249 | if (!QSystemTrayIcon::isSystemTrayAvailable()) { 250 | spdlog::error( FMT_STRING( "system tray unavailable" ) ); 251 | exit( -1); 252 | } 253 | 254 | m2iTrayIcon myIcon; 255 | QApplication::setQuitOnLastWindowClosed(false); 256 | 257 | #endif//WITH_QT 258 | 259 | /* ========================== Load Script =========================== */ 260 | spdlog::info( FMT_STRING( "LUA: Loading script: {}" ), m2i::script.c_str() ); 261 | if( luaL_loadfile( m2i::L, m2i::script.c_str() ) || lua_pcall( m2i::L, 0, 0, 0 ) ){ 262 | spdlog::critical( FMT_STRING( "LUA: failure loading script file: {}" ), lua_tostring( m2i::L, -1 ) ); 263 | lua_pop( m2i::L, 1 ); 264 | return -1; 265 | } else { 266 | m2i::notifier.watchPath({0, m2i::script, inotify_script_change}); 267 | } 268 | 269 | lua_getglobal( m2i::L, "script_init" ); 270 | if( lua_pcall( m2i::L, 0, 0, 0 ) != LUA_OK ){ 271 | spdlog::warn( FMT_STRING("LUA: Missing function: {}"), lua_tostring( m2i::L, -1) ); 272 | lua_pop( m2i::L, 1); 273 | } 274 | 275 | /* =========================== Main Loop ============================ */ 276 | spdlog::info( FMT_STRING( "Entering sleep, waiting for events" ) ); 277 | std::chrono::system_clock::time_point loop_last = std::chrono::system_clock::now(); 278 | std::chrono::system_clock::time_point watch_last = std::chrono::system_clock::now(); 279 | while(! m2i::quit ) 280 | { 281 | auto main_start = std::chrono::system_clock::now(); 282 | 283 | //this is the way it must be done if i want it to be completely optional 284 | #ifdef WITH_QT 285 | QCoreApplication::processEvents(); 286 | #endif 287 | 288 | #ifdef WITH_ALSA 289 | if( m2i::seq ){ 290 | while( m2i::seq.event_pending() > 0 ){ 291 | m2i::midi_to_lua(m2i::L, m2i::seq.event_receive()); 292 | } 293 | } 294 | #endif//WITH_ALSA 295 | 296 | #ifdef WITH_JACK 297 | if( m2i::jack.valid ){ 298 | while( m2i::jack.event_pending() > 0 ){ 299 | m2i::midi_to_lua(m2i::L, m2i::jack.event_receive()); 300 | } 301 | } 302 | #endif//WITH_JACK 303 | 304 | // run regular checks at watch_freq 305 | auto watch_wait = std::chrono::system_clock::now() - watch_last; 306 | if( watch_wait > m2i::watch_freq ){ 307 | watch_last = std::chrono::system_clock::now(); 308 | 309 | //notifier checks for modified files and runs the functions that 310 | //have been associated with them. 311 | m2i::notifier.check(); 312 | 313 | #ifdef WITH_JACK 314 | if( m2i::use_jack && m2i::reconnect && !m2i::jack.valid ){ 315 | // FIXME I'm not really happy with the re-initialisation of jack 316 | spdlog::error( FMT_STRING( "Jack not valid attempting to re-initialise" ) ); 317 | //attempt to re-instantiate jack connection 318 | m2i::jack.fina(); 319 | m2i::jack.init(); 320 | } 321 | #endif//WITH_JACK 322 | 323 | { 324 | int luastacksize = lua_gettop(m2i::L); 325 | if( luastacksize != 0 ) spdlog::info( FMT_STRING( "Lua Stack Size: {}" ), luastacksize); 326 | } 327 | } 328 | 329 | // run the loop lua function at loop_freq 330 | auto loop_wait = std::chrono::system_clock::now() - loop_last; 331 | if( m2i::loop_enabled && loop_wait > m2i::loop_freq ){ 332 | loop_last = std::chrono::system_clock::now(); 333 | lua_getglobal( m2i::L, "loop" ); 334 | if( lua_pcall( m2i::L, 0, 0, 0 ) != LUA_OK ){ 335 | spdlog::warn( FMT_STRING("LUA: Missing function: {}"), lua_tostring( m2i::L, -1) ); 336 | spdlog::error( FMT_STRING("LUA: Disabling loop function") ); 337 | m2i::loop_enabled = false; 338 | lua_pop( m2i::L, 1 ); 339 | } 340 | } 341 | 342 | //limit mainloop to m2i::main_freq 343 | auto main_end = std::chrono::system_clock::now(); 344 | std::chrono::duration< double > main_time = main_end - main_start; 345 | if( main_time > m2i::main_freq ){ 346 | spdlog::warn( FMT_STRING( "processing time of {:.6f}ms is longer than timer resolution" ), 347 | main_time.count() * 1000 ); 348 | } else { 349 | std::this_thread::sleep_for( m2i::main_freq - main_time ); 350 | } 351 | } 352 | 353 | lua_close( m2i::L ); 354 | exit( 0 ); 355 | } 356 | -------------------------------------------------------------------------------- /src/alsa.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "alsa.h" 4 | #include "util.h" 5 | 6 | using namespace m2i; 7 | 8 | int 9 | AlsaSeq::open() 10 | { 11 | if( snd_seq_open( &seq, "default", SND_SEQ_OPEN_DUPLEX, SND_SEQ_NONBLOCK ) < 0 ) 12 | return -1; 13 | client_id = snd_seq_client_id( seq ); 14 | snd_seq_set_client_name( seq, "midi2input_alsa" ); 15 | 16 | if( !seq ){ 17 | spdlog::error( FMT_STRING( "ALSA: Sequencer not initialised" ) ); 18 | return -1; 19 | } 20 | 21 | iport_id = snd_seq_create_simple_port( 22 | seq, 23 | "in", 24 | SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, 25 | SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION 26 | ); 27 | if( iport_id < 0 ){ 28 | spdlog::error( FMT_STRING( "ALSA: Problem creating input midi port" ) ); 29 | return -1; 30 | } 31 | 32 | oport_id = snd_seq_create_simple_port( 33 | seq, 34 | "out", 35 | SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, 36 | SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION 37 | ); 38 | if( oport_id < 0 ){ 39 | spdlog::error( FMT_STRING( "ALSA: Problem creating output midi port" ) ); 40 | return -1; 41 | } 42 | return client_id; 43 | } 44 | 45 | void 46 | AlsaSeq::close() 47 | { 48 | if( seq ){ 49 | snd_seq_delete_simple_port( seq, iport_id ); 50 | snd_seq_delete_simple_port( seq, oport_id ); 51 | snd_seq_close( seq ); 52 | } 53 | iport_id = -1; 54 | oport_id = -1; 55 | client_id = -1; 56 | seq = nullptr; 57 | } 58 | 59 | int 60 | AlsaSeq::connect(const std::string &client_name, const std::string &port_name ) 61 | { 62 | int connections = 0; 63 | //client info 64 | snd_seq_client_info_t *cinfo; 65 | snd_seq_client_info_alloca( &cinfo ); 66 | 67 | //port info 68 | snd_seq_port_info_t *pinfo; 69 | snd_seq_port_info_alloca( &pinfo ); 70 | 71 | snd_seq_client_info_set_client( cinfo, -1 ); 72 | while( snd_seq_query_next_client( seq, cinfo ) >= 0 ){ 73 | //refuse to self connect 74 | if( client_id == snd_seq_client_info_get_client( cinfo ) ){ 75 | continue; 76 | } 77 | if( client_name == "*" ){ 78 | spdlog::info( FMT_STRING( "ALSA: client wildcard match: {}" ), snd_seq_client_info_get_name( cinfo ) ); 79 | } 80 | else if( client_name == snd_seq_client_info_get_name( cinfo ) ){ 81 | spdlog::info( FMT_STRING( "ALSA: client name match: {}" ), snd_seq_client_info_get_name( cinfo ) ); 82 | } 83 | else continue; 84 | 85 | int client = snd_seq_client_info_get_client( cinfo ); 86 | snd_seq_port_info_set_client( pinfo, client ); 87 | snd_seq_port_info_set_port( pinfo, -1 ); 88 | while( snd_seq_query_next_port( seq, pinfo ) >= 0 ){ 89 | if( port_name == "*" ) 90 | spdlog::info( FMT_STRING( "ALSA: port wildcard match: {}" ), snd_seq_port_info_get_name( pinfo ) ); 91 | else if( port_name == snd_seq_port_info_get_name( pinfo ) ) 92 | spdlog::info( FMT_STRING( "ALSA: port name match: {}" ), snd_seq_port_info_get_name( pinfo ) ); 93 | else continue; 94 | 95 | auto capabilities = snd_seq_port_info_get_capability(pinfo); 96 | if( capabilities & (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ) ){ 97 | spdlog::info( FMT_STRING( "ALSA: port connect from: {}" ), snd_seq_port_info_get_name( pinfo ) ); 98 | snd_seq_connect_from( seq, iport_id, client, snd_seq_port_info_get_port( pinfo ) ); 99 | connections++; 100 | } 101 | if( capabilities & (SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE) ){ 102 | spdlog::info( FMT_STRING( "ALSA: port connect to: {}" ), snd_seq_port_info_get_name( pinfo ) ); 103 | snd_seq_connect_to( seq, oport_id, client, snd_seq_port_info_get_port( pinfo ) ); 104 | connections++; 105 | } 106 | } 107 | } 108 | return connections; 109 | } 110 | 111 | void 112 | AlsaSeq::event_send(const midi_event &event ) 113 | { 114 | snd_seq_event_t ev; 115 | snd_seq_ev_clear( &ev ); 116 | snd_seq_ev_set_source( &ev, oport_id ); 117 | snd_seq_ev_set_subs( &ev ); 118 | snd_seq_ev_set_direct( &ev ); 119 | 120 | switch( event.status & 0xF0 ){ 121 | case 0x80: 122 | snd_seq_ev_set_noteoff( &ev, event.status & 0x0F, event.data1, event.data2 ); 123 | spdlog::info( FMT_STRING( "ALSA: send_noteoff: {}" ), event.str() ); 124 | break; 125 | case 0x90: 126 | snd_seq_ev_set_noteon( &ev, event.status & 0x0F, event.data1, event.data2 ); 127 | spdlog::info( FMT_STRING( "ALSA: send_noteon: {}" ), event.str() ); 128 | break; 129 | case 0xB0: 130 | snd_seq_ev_set_controller( &ev, event.status & 0x0F, event.data1, event.data2 ); 131 | spdlog::info( FMT_STRING( "ALSA: send_control: {}" ), event.str() ); 132 | break; 133 | default: 134 | spdlog::info( FMT_STRING( "ALSA: send type({}) not supported" ), event.status & 0xF0 ); 135 | return; 136 | } 137 | 138 | snd_seq_event_output( seq, &ev); 139 | snd_seq_drain_output( seq ); 140 | } 141 | 142 | int 143 | AlsaSeq::event_pending(){ 144 | return snd_seq_event_input_pending( seq, 1 ); 145 | } 146 | 147 | midi_event 148 | AlsaSeq::event_receive() 149 | { 150 | midi_event result = { 0, 0, 0 }; 151 | snd_seq_event_t *ev = nullptr; 152 | if( snd_seq_event_input( seq, &ev ) < 0) 153 | return result; 154 | 155 | switch( ev->type ){ 156 | case SND_SEQ_EVENT_NOTEON: 157 | spdlog::info( FMT_STRING( "ALSA: Note On: {:#04x}, {:#04x}, {:3d}" ), 158 | ev->data.note.channel, ev->data.note.note, ev->data.note.velocity ); 159 | result.status = ev->data.note.channel + 0x90; 160 | result.data1 = ev->data.note.note; 161 | result.data2 = ev->data.note.velocity; 162 | break; 163 | case SND_SEQ_EVENT_NOTEOFF: 164 | spdlog::info( FMT_STRING( "ALSA: Note Off: {:#04x}, {:#04x}, {:3d}" ), 165 | ev->data.note.channel, ev->data.note.note, ev->data.note.velocity ); 166 | result.status = ev->data.note.channel + 0x80; 167 | result.data1 = ev->data.note.note; 168 | result.data2 = ev->data.note.velocity; 169 | break; 170 | case SND_SEQ_EVENT_KEYPRESS: 171 | spdlog::info( FMT_STRING( "ALSA: Polyphonic aftertouch: {}, {}, {}" ), 172 | ev->data.note.channel, ev->data.note.note, ev->data.note.velocity ); 173 | break; 174 | case SND_SEQ_EVENT_CONTROLLER: 175 | spdlog::info( FMT_STRING( "ALSA: Control Change: {:#04x}, {:#04x}, {:3d}" ), 176 | ev->data.control.channel, ev->data.control.param, ev->data.control.value ); 177 | result.status = ev->data.control.channel + 0xB0; 178 | result.data1 = ev->data.control.param; 179 | result.data2 = ev->data.control.value; 180 | break; 181 | case SND_SEQ_EVENT_PGMCHANGE: 182 | spdlog::info( FMT_STRING( "ALSA: Program change: {}, {}" ), 183 | ev->data.control.channel, ev->data.control.value ); 184 | break; 185 | case SND_SEQ_EVENT_CHANPRESS: 186 | spdlog::info( FMT_STRING( "ALSA: Channel aftertouch: {}, {}" ), 187 | ev->data.control.channel, ev->data.control.value ); 188 | break; 189 | case SND_SEQ_EVENT_PITCHBEND: 190 | spdlog::info( FMT_STRING( "ALSA: Pitch bend: {}, {}" ), 191 | ev->data.control.channel, ev->data.control.value ); 192 | result.status = ev->data.control.channel + 0xE0; 193 | //FIXME this really sucks i'm throwing away so much detail it breaks my heart. 194 | //also its just shitty shitty code 195 | result.data2 = (ev->data.control.value + 8192) / 64; 196 | break; 197 | case SND_SEQ_EVENT_CONTROL14: 198 | spdlog::info( FMT_STRING( "ALSA: Control change: {}, {}, {}" ), 199 | ev->data.control.channel, ev->data.control.param, ev->data.control.value ); 200 | break; 201 | case SND_SEQ_EVENT_NONREGPARAM: 202 | spdlog::info( FMT_STRING( "ALSA: Non-reg. parameter: {}, {}, {}" ), 203 | ev->data.control.channel, ev->data.control.param, ev->data.control.value ); 204 | break; 205 | case SND_SEQ_EVENT_REGPARAM: 206 | spdlog::info( FMT_STRING( "ALSA: Reg. parameter {}, {}, {}" ), 207 | ev->data.control.channel, ev->data.control.param, ev->data.control.value ); 208 | break; 209 | case SND_SEQ_EVENT_SONGPOS: 210 | spdlog::info( FMT_STRING( "ALSA: Song position pointer: {}" ), ev->data.control.value ); 211 | break; 212 | case SND_SEQ_EVENT_SONGSEL: 213 | spdlog::info( FMT_STRING( "ALSA: Song select: {}" ), ev->data.control.value ); 214 | break; 215 | case SND_SEQ_EVENT_QFRAME: 216 | spdlog::info( FMT_STRING( "ALSA: MTC quarter frame: {}" ), ev->data.control.value ); 217 | break; 218 | case SND_SEQ_EVENT_TIMESIGN: 219 | spdlog::info( FMT_STRING( "ALSA: SMF time signature: {}" ), ev->data.control.value ); 220 | break; 221 | case SND_SEQ_EVENT_KEYSIGN: 222 | spdlog::info( FMT_STRING( "ALSA: SMF key signature: {}" ), ev->data.control.value ); 223 | break; 224 | case SND_SEQ_EVENT_START: 225 | if( ev->source.client == SND_SEQ_CLIENT_SYSTEM && 226 | ev->source.port == SND_SEQ_PORT_SYSTEM_TIMER ) 227 | spdlog::info( FMT_STRING( "ALSA: Queue start: {}" ), ev->data.queue.queue ); 228 | else 229 | spdlog::info( FMT_STRING( "ALSA: Start" ) ); 230 | break; 231 | case SND_SEQ_EVENT_CONTINUE: 232 | if( ev->source.client == SND_SEQ_CLIENT_SYSTEM && 233 | ev->source.port == SND_SEQ_PORT_SYSTEM_TIMER ) 234 | spdlog::info( FMT_STRING( "ALSA: Queue continue: {}" ), ev->data.queue.queue ); 235 | else 236 | spdlog::info( FMT_STRING( "ALSA: Continue" ) ); 237 | break; 238 | case SND_SEQ_EVENT_STOP: 239 | if( ev->source.client == SND_SEQ_CLIENT_SYSTEM && 240 | ev->source.port == SND_SEQ_PORT_SYSTEM_TIMER ) 241 | spdlog::info( FMT_STRING( "ALSA: Queue stop: {}" ), ev->data.queue.queue ); 242 | else 243 | spdlog::info( FMT_STRING( "ALSA: Stop" ) ); 244 | break; 245 | case SND_SEQ_EVENT_SETPOS_TICK: 246 | spdlog::info( FMT_STRING( "ALSA: Set tick queue pos: {}" ), ev->data.queue.queue ); 247 | break; 248 | case SND_SEQ_EVENT_SETPOS_TIME: 249 | spdlog::info( FMT_STRING( "ALSA: Set rt queue pos: {}" ), ev->data.queue.queue ); 250 | break; 251 | case SND_SEQ_EVENT_TEMPO: 252 | spdlog::info( FMT_STRING( "ALSA: Set queue tempo: {}" ), ev->data.queue.queue ); 253 | break; 254 | case SND_SEQ_EVENT_CLOCK: 255 | spdlog::info( FMT_STRING( "ALSA: Clock" ) ); 256 | break; 257 | case SND_SEQ_EVENT_TICK: 258 | spdlog::info( FMT_STRING( "ALSA: Tick" ) ); 259 | break; 260 | case SND_SEQ_EVENT_QUEUE_SKEW: 261 | spdlog::info( FMT_STRING( "ALSA: Queue timer skew: {}" ), ev->data.queue.queue ); 262 | break; 263 | case SND_SEQ_EVENT_TUNE_REQUEST: 264 | spdlog::info( FMT_STRING( "ALSA: Tune request" ) ); 265 | break; 266 | case SND_SEQ_EVENT_RESET: 267 | spdlog::info( FMT_STRING( "ALSA: Reset" ) ); 268 | break; 269 | case SND_SEQ_EVENT_SENSING: 270 | spdlog::info( FMT_STRING( "ALSA: Active Sensing" ) ); 271 | break; 272 | case SND_SEQ_EVENT_CLIENT_START: 273 | spdlog::info( FMT_STRING( "ALSA: Client start: {}" ), ev->data.addr.client ); 274 | break; 275 | case SND_SEQ_EVENT_CLIENT_EXIT: 276 | spdlog::info( FMT_STRING( "ALSA: Client exit: {}" ), ev->data.addr.client ); 277 | break; 278 | case SND_SEQ_EVENT_CLIENT_CHANGE: 279 | spdlog::info( FMT_STRING( "ALSA: Client changed: {}" ), ev->data.addr.client ); 280 | break; 281 | case SND_SEQ_EVENT_PORT_START: 282 | spdlog::info( FMT_STRING( "ALSA: Port start: {},{}" ), ev->data.addr.client, ev->data.addr.port ); 283 | break; 284 | case SND_SEQ_EVENT_PORT_EXIT: 285 | spdlog::info( FMT_STRING( "ALSA: Port exit: {},{}" ), ev->data.addr.client, ev->data.addr.port ); 286 | break; 287 | case SND_SEQ_EVENT_PORT_CHANGE: 288 | spdlog::info( FMT_STRING( "ALSA: Port changed: {},{}" ), ev->data.addr.client, ev->data.addr.port ); 289 | break; 290 | case SND_SEQ_EVENT_PORT_SUBSCRIBED: 291 | { 292 | snd_seq_client_info_t *cinfo; 293 | snd_seq_client_info_alloca( &cinfo ); 294 | snd_seq_port_info_t *pinfo; 295 | snd_seq_port_info_alloca( &pinfo ); 296 | 297 | snd_seq_get_any_client_info( seq, 298 | ev->data.connect.sender.client , 299 | cinfo ); 300 | snd_seq_get_any_port_info( seq, 301 | ev->data.connect.sender.client, 302 | ev->data.connect.sender.port, 303 | pinfo ); 304 | 305 | spdlog::info( FMT_STRING( "ALSA: Port Subscription from: '{}':'{}'" ), 306 | snd_seq_client_info_get_name( cinfo ), 307 | snd_seq_port_info_get_name( pinfo ) 308 | ); 309 | spdlog::info( FMT_STRING( "ALSA: Port Subscribed: {:d}:{:d} -> {:d}:{:d}" ), 310 | ev->data.connect.sender.client, ev->data.connect.sender.port, 311 | ev->data.connect.dest.client, ev->data.connect.dest.port ); 312 | } 313 | break; 314 | case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: 315 | spdlog::info( FMT_STRING( "ALSA: Port unsubscribed: {:d}:{:d} -> {:d}:{:d}" ), 316 | ev->data.connect.sender.client, ev->data.connect.sender.port, 317 | ev->data.connect.dest.client, ev->data.connect.dest.port ); 318 | break; 319 | case SND_SEQ_EVENT_SYSEX: 320 | { 321 | unsigned int i; 322 | spdlog::info( FMT_STRING( "ALSA: System exclusive: " )); 323 | for (i = 0; i < ev->data.ext.len; ++i) 324 | fmt::format( "{}", static_cast(ev->data.ext.ptr)[i] ); 325 | fmt::format("\n"); 326 | } 327 | break; 328 | default: 329 | spdlog::info( FMT_STRING( "ALSA: Event type: {}" ), ev->type ); 330 | } 331 | return result; 332 | } 333 | 334 | AlsaSeq::~AlsaSeq() { 335 | close(); 336 | } 337 | -------------------------------------------------------------------------------- /include/argh.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace argh 12 | { 13 | // Terminology: 14 | // A command line is composed of 2 types of args: 15 | // 1. Positional args, i.e. free standing values 16 | // 2. Options: args beginning with '-'. We identify two kinds: 17 | // 2.1: Flags: boolean options => (exist ? true : false) 18 | // 2.2: Parameters: a name followed by a non-option value 19 | 20 | #if !defined(__GNUC__) || (__GNUC__ >= 5) 21 | using string_stream = std::istringstream; 22 | #else 23 | // Until GCC 5, istringstream did not have a move constructor. 24 | // stringstream_proxy is used instead, as a workaround. 25 | class stringstream_proxy 26 | { 27 | public: 28 | stringstream_proxy() = default; 29 | 30 | // Construct with a value. 31 | stringstream_proxy(std::string const& value) : 32 | stream_(value) 33 | {} 34 | 35 | // Copy constructor. 36 | stringstream_proxy(const stringstream_proxy& other) : 37 | stream_(other.stream_.str()) 38 | { 39 | stream_.setstate(other.stream_.rdstate()); 40 | } 41 | 42 | void setstate(std::ios_base::iostate state) { stream_.setstate(state); } 43 | 44 | // Stream out the value of the parameter. 45 | // If the conversion was not possible, the stream will enter the fail state, 46 | // and operator bool will return false. 47 | template 48 | stringstream_proxy& operator >> (T& thing) 49 | { 50 | stream_ >> thing; 51 | return *this; 52 | } 53 | 54 | 55 | // Get the string value. 56 | std::string str() const { return stream_.str(); } 57 | 58 | std::stringbuf* rdbuf() const { return stream_.rdbuf(); } 59 | 60 | // Check the state of the stream. 61 | // False when the most recent stream operation failed 62 | operator bool() const { return !!stream_; } 63 | 64 | ~stringstream_proxy() = default; 65 | private: 66 | std::istringstream stream_; 67 | }; 68 | using string_stream = stringstream_proxy; 69 | #endif 70 | 71 | class parser 72 | { 73 | public: 74 | enum Mode { PREFER_FLAG_FOR_UNREG_OPTION = 1 << 0, 75 | PREFER_PARAM_FOR_UNREG_OPTION = 1 << 1, 76 | NO_SPLIT_ON_EQUALSIGN = 1 << 2, 77 | SINGLE_DASH_IS_MULTIFLAG = 1 << 3, 78 | }; 79 | 80 | parser() = default; 81 | 82 | parser(std::initializer_list pre_reg_names) 83 | { add_params(pre_reg_names); } 84 | 85 | parser(const char* const argv[], int mode = PREFER_FLAG_FOR_UNREG_OPTION) 86 | { parse(argv, mode); } 87 | 88 | parser(int argc, const char* const argv[], int mode = PREFER_FLAG_FOR_UNREG_OPTION) 89 | { parse(argc, argv, mode); } 90 | 91 | void add_param(std::string const& name); 92 | void add_params(std::initializer_list init_list); 93 | 94 | void parse(const char* const argv[], int mode = PREFER_FLAG_FOR_UNREG_OPTION); 95 | void parse(int argc, const char* const argv[], int mode = PREFER_FLAG_FOR_UNREG_OPTION); 96 | 97 | std::multiset const& flags() const { return flags_; } 98 | std::map const& params() const { return params_; } 99 | std::vector const& pos_args() const { return pos_args_; } 100 | 101 | // begin() and end() for using range-for over positional args. 102 | std::vector::const_iterator begin() const { return pos_args_.cbegin(); } 103 | std::vector::const_iterator end() const { return pos_args_.cend(); } 104 | 105 | ////////////////////////////////////////////////////////////////////////// 106 | // Accessors 107 | 108 | // flag (boolean) accessors: return true if the flag appeared, otherwise false. 109 | bool operator[](std::string const& name) const; 110 | 111 | // multiple flag (boolean) accessors: return true if at least one of the flag appeared, otherwise false. 112 | bool operator[](std::initializer_list init_list) const; 113 | 114 | // returns positional arg string by order. Like argv[] but without the options 115 | std::string const& operator[](size_t ind) const; 116 | 117 | // returns a std::istream that can be used to convert a positional arg to a typed value. 118 | string_stream operator()(size_t ind) const; 119 | 120 | // same as above, but with a default value in case the arg is missing (index out of range). 121 | template 122 | string_stream operator()(size_t ind, T&& def_val) const; 123 | 124 | // parameter accessors, give a name get an std::istream that can be used to convert to a typed value. 125 | // call .str() on result to get as string 126 | string_stream operator()(std::string const& name) const; 127 | 128 | // accessor for a parameter with multiple names, give a list of names, get an std::istream that can be used to convert to a typed value. 129 | // call .str() on result to get as string 130 | // returns the first value in the list to be found. 131 | string_stream operator()(std::initializer_list init_list) const; 132 | 133 | // same as above, but with a default value in case the param was missing. 134 | // Non-string def_val types must have an operator<<() (output stream operator) 135 | // If T only has an input stream operator, pass the string version of the type as in "3" instead of 3. 136 | template 137 | string_stream operator()(std::string const& name, T&& def_val) const; 138 | 139 | // same as above but for a list of names. returns the first value to be found. 140 | template 141 | string_stream operator()(std::initializer_list init_list, T&& def_val) const; 142 | 143 | private: 144 | string_stream bad_stream() const; 145 | std::string trim_leading_dashes(std::string const& name) const; 146 | bool is_number(std::string const& arg) const; 147 | bool is_option(std::string const& arg) const; 148 | bool got_flag(std::string const& name) const; 149 | 150 | private: 151 | std::vector args_; 152 | std::map params_; 153 | std::vector pos_args_; 154 | std::multiset flags_; 155 | std::set registeredParams_; 156 | std::string empty_; 157 | }; 158 | 159 | 160 | ////////////////////////////////////////////////////////////////////////// 161 | 162 | inline void parser::parse(const char * const argv[], int mode) 163 | { 164 | int argc = 0; 165 | for (auto argvp = argv; *argvp; ++argc, ++argvp); 166 | parse(argc, argv, mode); 167 | } 168 | 169 | ////////////////////////////////////////////////////////////////////////// 170 | 171 | void parser::parse(int argc, const char* const argv[], int mode /*= PREFER_FLAG_FOR_UNREG_OPTION*/) 172 | { 173 | // convert to strings 174 | args_.resize(argc); 175 | std::transform(argv, argv + argc, args_.begin(), [](const char* const arg) { return arg; }); 176 | 177 | // parse line 178 | for (auto i = 0u; i < args_.size(); ++i) 179 | { 180 | if (!is_option(args_[i])) 181 | { 182 | pos_args_.emplace_back(args_[i]); 183 | continue; 184 | } 185 | 186 | auto name = trim_leading_dashes(args_[i]); 187 | 188 | if (!(mode & NO_SPLIT_ON_EQUALSIGN)) 189 | { 190 | auto equalPos = name.find('='); 191 | if (equalPos != std::string::npos) 192 | { 193 | params_.insert({ name.substr(0, equalPos), name.substr(equalPos + 1) }); 194 | continue; 195 | } 196 | } 197 | 198 | // if the option is unregistered and should be a multi-flag 199 | if (1 == (args_[i].size() - name.size()) && // single dash 200 | argh::parser::SINGLE_DASH_IS_MULTIFLAG & mode && // multi-flag mode 201 | registeredParams_.find(name) == registeredParams_.end()) // unregistered 202 | { 203 | for (auto const& c : name) 204 | { 205 | flags_.emplace(std::string{ c }); 206 | } 207 | } 208 | 209 | // any potential option will get as its value the next arg, unless that arg is an option too 210 | // in that case it will be determined a flag. 211 | if (i == args_.size() - 1 || is_option(args_[i + 1])) 212 | { 213 | flags_.emplace(name); 214 | continue; 215 | } 216 | 217 | // if 'name' is a pre-registered option, then the next arg cannot be a free parameter to it is skipped 218 | // otherwise we have 2 modes: 219 | // PREFER_FLAG_FOR_UNREG_OPTION: a non-registered 'name' is determined a flag. 220 | // The following value (the next arg) will be a free parameter. 221 | // 222 | // PREFER_PARAM_FOR_UNREG_OPTION: a non-registered 'name' is determined a parameter, the next arg 223 | // will be the value of that option. 224 | 225 | if (registeredParams_.find(name) != registeredParams_.end() || 226 | argh::parser::PREFER_PARAM_FOR_UNREG_OPTION & mode) 227 | { 228 | params_.insert({ name, args_[i + 1] }); 229 | ++i; // skip next value, it is not a free parameter 230 | continue; 231 | } 232 | 233 | if (argh::parser::PREFER_FLAG_FOR_UNREG_OPTION & mode) 234 | flags_.emplace(name); 235 | }; 236 | } 237 | 238 | ////////////////////////////////////////////////////////////////////////// 239 | 240 | string_stream parser::bad_stream() const 241 | { 242 | string_stream bad; 243 | bad.setstate(std::ios_base::failbit); 244 | return bad; 245 | } 246 | 247 | ////////////////////////////////////////////////////////////////////////// 248 | 249 | bool parser::is_number(std::string const& arg) const 250 | { 251 | // inefficient but simple way to determine if a string is a number (which can start with a '-') 252 | std::istringstream istr(arg); 253 | double number; 254 | istr >> number; 255 | return !(istr.fail() || istr.bad()); 256 | } 257 | 258 | ////////////////////////////////////////////////////////////////////////// 259 | 260 | bool parser::is_option(std::string const& arg) const 261 | { 262 | assert(0 != arg.size()); 263 | if (is_number(arg)) 264 | return false; 265 | return '-' == arg[0]; 266 | } 267 | 268 | ////////////////////////////////////////////////////////////////////////// 269 | 270 | std::string parser::trim_leading_dashes(std::string const& name) const 271 | { 272 | auto pos = name.find_first_not_of('-'); 273 | return std::string::npos != pos ? name.substr(pos) : name; 274 | } 275 | 276 | ////////////////////////////////////////////////////////////////////////// 277 | 278 | bool argh::parser::got_flag(std::string const& name) const 279 | { 280 | return flags_.end() != flags_.find(trim_leading_dashes(name)); 281 | } 282 | 283 | ////////////////////////////////////////////////////////////////////////// 284 | 285 | bool parser::operator[](std::string const& name) const 286 | { 287 | return got_flag(name); 288 | } 289 | 290 | ////////////////////////////////////////////////////////////////////////// 291 | 292 | bool parser::operator[](std::initializer_list init_list) const 293 | { 294 | return std::any_of(init_list.begin(), init_list.end(), [&](char const* const name) { return got_flag(name); }); 295 | } 296 | 297 | ////////////////////////////////////////////////////////////////////////// 298 | 299 | std::string const& parser::operator[](size_t ind) const 300 | { 301 | if (ind < pos_args_.size()) 302 | return pos_args_[ind]; 303 | return empty_; 304 | } 305 | 306 | ////////////////////////////////////////////////////////////////////////// 307 | 308 | string_stream parser::operator()(std::string const& name) const 309 | { 310 | auto optIt = params_.find(trim_leading_dashes(name)); 311 | if (params_.end() != optIt) 312 | return string_stream(optIt->second); 313 | return bad_stream(); 314 | } 315 | 316 | ////////////////////////////////////////////////////////////////////////// 317 | 318 | string_stream parser::operator()(std::initializer_list init_list) const 319 | { 320 | for (auto& name : init_list) 321 | { 322 | auto optIt = params_.find(trim_leading_dashes(name)); 323 | if (params_.end() != optIt) 324 | return string_stream(optIt->second); 325 | } 326 | return bad_stream(); 327 | } 328 | 329 | ////////////////////////////////////////////////////////////////////////// 330 | 331 | template 332 | string_stream parser::operator()(std::string const& name, T&& def_val) const 333 | { 334 | auto optIt = params_.find(trim_leading_dashes(name)); 335 | if (params_.end() != optIt) 336 | return string_stream(optIt->second); 337 | 338 | std::ostringstream ostr; 339 | ostr << def_val; 340 | return string_stream(ostr.str()); // use default 341 | } 342 | 343 | ////////////////////////////////////////////////////////////////////////// 344 | 345 | // same as above but for a list of names. returns the first value to be found. 346 | template 347 | string_stream parser::operator()(std::initializer_list init_list, T&& def_val) const 348 | { 349 | for (auto& name : init_list) 350 | { 351 | auto optIt = params_.find(trim_leading_dashes(name)); 352 | if (params_.end() != optIt) 353 | return string_stream(optIt->second); 354 | } 355 | std::ostringstream ostr; 356 | ostr << def_val; 357 | return string_stream(ostr.str()); // use default 358 | } 359 | 360 | ////////////////////////////////////////////////////////////////////////// 361 | 362 | string_stream parser::operator()(size_t ind) const 363 | { 364 | if (pos_args_.size() <= ind) 365 | return bad_stream(); 366 | 367 | return string_stream(pos_args_[ind]); 368 | } 369 | 370 | ////////////////////////////////////////////////////////////////////////// 371 | 372 | template 373 | string_stream parser::operator()(size_t ind, T&& def_val) const 374 | { 375 | if (pos_args_.size() <= ind) 376 | { 377 | std::ostringstream ostr; 378 | ostr << def_val; 379 | return string_stream(ostr.str()); 380 | } 381 | 382 | return string_stream(pos_args_[ind]); 383 | } 384 | 385 | ////////////////////////////////////////////////////////////////////////// 386 | 387 | void parser::add_param(std::string const& name) 388 | { 389 | registeredParams_.insert(trim_leading_dashes(name)); 390 | } 391 | 392 | ////////////////////////////////////////////////////////////////////////// 393 | 394 | void parser::add_params(std::initializer_list init_list) 395 | { 396 | for (auto& name : init_list) 397 | registeredParams_.insert(trim_leading_dashes(name)); 398 | } 399 | } 400 | 401 | 402 | -------------------------------------------------------------------------------- /cfg/Denon-DN-SC2000.lua: -------------------------------------------------------------------------------- 1 | --[[ Imported c++ Functions ]]-- 2 | --midi_send( { status, data1, data2 } ) 3 | --keypress( XK_keycode ) 4 | --keydown( XK_keycode ) 5 | --keyup( XK_keycode ) 6 | --buttonpress( n ) 7 | --buttondown( n ) 8 | --buttonup( n ) 9 | --mousemove( x, y ) 10 | --mousepos( x, y ) 11 | --exec( 'command' ) 12 | -- 13 | --[[ Imported Global Variables ]]-- 14 | --WM_CLASS 15 | -- 16 | --[[ Functions you want to define yourself]]-- 17 | --midi_recv( status, data1, data2 ) 18 | --pre_loop() 19 | --loop() 20 | 21 | --[[ helper functions]]-- 22 | --[[=================]]-- 23 | -- 24 | --[[ MSB LSB ]]-- 25 | -- asign the MSB function to the MSB event in the event map 26 | -- assign your function to the LSB event 27 | -- use the LSB function in your function to get the final value 28 | msb = nil 29 | function MSB( event ) 30 | msb = event[3] 31 | end 32 | 33 | function LSB( event ) 34 | if( not msb ) then 35 | print( 'no MSB to gather' ) 36 | return 37 | end 38 | local final = (msb * 256) + event[3] 39 | print( final ) 40 | msb = nil 41 | return final 42 | end 43 | 44 | function reset_state( state ) 45 | --TODO loop through the midi state map and set all the lights to their default 46 | end 47 | 48 | -- because i always give the event to functions i needed a wrapper for keypress 49 | function kpress( event, keys ) 50 | if( type( keys ) == "number" ) then 51 | print("single key event") 52 | keypress( keys ) 53 | return 54 | end 55 | --FIXME needs one shot combo shortcut implementation 56 | if( type( keys ) == "table" ) then 57 | print( "multi key event" ) 58 | kdown( event, keys ) 59 | -- FIXME need to reverse the list here 60 | kup( event, keys ) 61 | return 62 | end 63 | print( "Unhandled type: " .. type( key ) ) 64 | end 65 | 66 | 67 | function kdown( event, keys ) 68 | print( "kdown function" ) 69 | if( type( keys ) == "number" ) then 70 | print("single key event") 71 | keydown( keys ) 72 | return 73 | end 74 | if( type( keys ) == "table" ) then 75 | print( "multi key event" ) 76 | for i,key in pairs(keys) do 77 | keydown( key ) 78 | end 79 | return 80 | end 81 | print( "Unhandled type: " .. type( key ) ) 82 | end 83 | 84 | function kup( event, keys ) 85 | print( "kup function" ) 86 | if( type( keys ) == "number" ) then 87 | print("single key event") 88 | keyup( keys ) 89 | return 90 | end 91 | if( type( keys ) == "table" ) then 92 | print( "multi key event" ) 93 | for i,key in pairs(keys) do 94 | keyup( key ) 95 | end 96 | return 97 | end 98 | print( "Unhandled type: " .. type( key ) ) 99 | end 100 | 101 | function event_to_string( event ) 102 | return string.format("0x%2X, 0x%2X, %3d", event[1], event[2], event[3] ) 103 | end 104 | 105 | --[[ keystrokes defined by linux\input-event-codes.h]]-- 106 | KEY_RESERVED = 0 107 | KEY_ESC = 1 108 | KEY_1 = 2 109 | KEY_2 = 3 110 | KEY_3 = 4 111 | KEY_4 = 5 112 | KEY_5 = 6 113 | KEY_6 = 7 114 | KEY_7 = 8 115 | KEY_8 = 9 116 | KEY_9 = 10 117 | KEY_0 = 11 118 | KEY_MINUS = 12 119 | KEY_EQUAL = 13 120 | KEY_BACKSPACE = 14 121 | KEY_TAB = 15 122 | KEY_Q = 16 123 | KEY_W = 17 124 | KEY_E = 18 125 | KEY_R = 19 126 | KEY_T = 20 127 | KEY_Y = 21 128 | KEY_U = 22 129 | KEY_I = 23 130 | KEY_O = 24 131 | KEY_P = 25 132 | KEY_LEFTBRACE = 26 133 | KEY_RIGHTBRACE = 27 134 | KEY_ENTER = 28 135 | KEY_LEFTCTRL = 29 136 | KEY_A = 30 137 | KEY_S = 31 138 | KEY_D = 32 139 | KEY_F = 33 140 | KEY_G = 34 141 | KEY_H = 35 142 | KEY_J = 36 143 | KEY_K = 37 144 | KEY_L = 38 145 | KEY_SEMICOLON = 39 146 | KEY_APOSTROPHE = 40 147 | KEY_GRAVE = 41 148 | KEY_LEFTSHIFT = 42 149 | KEY_BACKSLASH = 43 150 | KEY_Z = 44 151 | KEY_X = 45 152 | KEY_C = 46 153 | KEY_V = 47 154 | KEY_B = 48 155 | KEY_N = 49 156 | KEY_M = 50 157 | KEY_COMMA = 51 158 | KEY_DOT = 52 159 | KEY_SLASH = 53 160 | KEY_RIGHTSHIFT = 54 161 | KEY_KPASTERISK = 55 162 | KEY_LEFTALT = 56 163 | KEY_SPACE = 57 164 | KEY_CAPSLOCK = 58 165 | KEY_F1 = 59 166 | KEY_F2 = 60 167 | KEY_F3 = 61 168 | KEY_F4 = 62 169 | KEY_F5 = 63 170 | KEY_F6 = 64 171 | KEY_F7 = 65 172 | KEY_F8 = 66 173 | KEY_F9 = 67 174 | KEY_F10 = 68 175 | KEY_NUMLOCK = 69 176 | KEY_SCROLLLOCK = 70 177 | KEY_KP7 = 71 178 | KEY_KP8 = 72 179 | KEY_KP9 = 73 180 | KEY_KPMINUS = 74 181 | KEY_KP4 = 75 182 | KEY_KP5 = 76 183 | KEY_KP6 = 77 184 | KEY_KPPLUS = 78 185 | KEY_KP1 = 79 186 | KEY_KP2 = 80 187 | KEY_KP3 = 81 188 | KEY_KP0 = 82 189 | KEY_KPDOT = 83 190 | 191 | KEY_ZENKAKUHANKAKU = 85 192 | KEY_102ND = 86 193 | KEY_F11 = 87 194 | KEY_F12 = 88 195 | KEY_RO = 89 196 | KEY_KATAKANA = 90 197 | KEY_HIRAGANA = 91 198 | KEY_HENKAN = 92 199 | KEY_KATAKANAHIRAGANA = 93 200 | KEY_MUHENKAN = 94 201 | KEY_KPJPCOMMA = 95 202 | KEY_KPENTER = 96 203 | KEY_RIGHTCTRL = 97 204 | KEY_KPSLASH = 98 205 | KEY_SYSRQ = 99 206 | KEY_RIGHTALT = 100 207 | KEY_LINEFEED = 101 208 | KEY_HOME = 102 209 | KEY_UP = 103 210 | KEY_PAGEUP = 104 211 | KEY_LEFT = 105 212 | KEY_RIGHT = 106 213 | KEY_END = 107 214 | KEY_DOWN = 108 215 | KEY_PAGEDOWN = 109 216 | KEY_INSERT = 110 217 | KEY_DELETE = 111 218 | KEY_MACRO = 112 219 | KEY_MUTE = 113 220 | KEY_VOLUMEDOWN = 114 221 | KEY_VOLUMEUP = 115 222 | KEY_POWER = 116 --[ SC System Power Down ]-- 223 | KEY_KPEQUAL = 117 224 | KEY_KPPLUSMINUS = 118 225 | KEY_PAUSE = 119 226 | KEY_SCALE = 120 --[ AL Compiz Scale (Expose) 227 | KEY_KPCOMMA = 121 228 | KEY_HANGEUL = 122 229 | KEY_HANGUEL = KEY_HANGEUL 230 | KEY_HANJA = 123 231 | KEY_YEN = 124 232 | KEY_LEFTMETA = 125 233 | KEY_RIGHTMETA = 126 234 | KEY_COMPOSE = 127 235 | KEY_STOP = 128 --[ AC Stop ]-- 236 | KEY_AGAIN = 129 237 | KEY_PROPS = 130 --[ AC Properties ]-- 238 | KEY_UNDO = 131 --[ AC Undo ]-- 239 | KEY_FRONT = 132 240 | KEY_COPY = 133 --[ AC Copy ]-- 241 | KEY_OPEN = 134 --[ AC Open ]-- 242 | KEY_PASTE = 135 --[ AC Paste ]-- 243 | KEY_FIND = 136 --[ AC Search ]-- 244 | KEY_CUT = 137 --[ AC Cut ]-- 245 | KEY_HELP = 138 --[ AL Integrated Help Center ]-- 246 | KEY_MENU = 139 --[ Menu (show menu) ]-- 247 | KEY_CALC = 140 --[ AL Calculator ]-- 248 | KEY_SETUP = 141 249 | KEY_SLEEP = 142 --[ SC System Sleep ]-- 250 | KEY_WAKEUP = 143 --[ System Wake Up ]-- 251 | KEY_FILE = 144 --[ AL Local Machine Browser ]-- 252 | KEY_SENDFILE = 145 253 | KEY_DELETEFILE = 146 254 | KEY_XFER = 147 255 | KEY_PROG1 = 148 256 | KEY_PROG2 = 149 257 | KEY_WWW = 150 --[ AL Internet Browser ]-- 258 | KEY_MSDOS = 151 259 | KEY_COFFEE = 152 --[ AL Terminal Lock/Screensaver ]-- 260 | KEY_SCREENLOCK = KEY_COFFEE 261 | KEY_ROTATE_DISPLAY = 153 --[ Display orientation for e.g. tablets ]-- 262 | KEY_DIRECTION = KEY_ROTATE_DISPLAY 263 | KEY_CYCLEWINDOWS = 154 264 | KEY_MAIL = 155 265 | KEY_BOOKMARKS = 156 --[ AC Bookmarks ]-- 266 | KEY_COMPUTER = 157 267 | KEY_BACK = 158 --[ AC Back ]-- 268 | KEY_FORWARD = 159 --[ AC Forward ]-- 269 | KEY_CLOSECD = 160 270 | KEY_EJECTCD = 161 271 | KEY_EJECTCLOSECD = 162 272 | KEY_NEXTSONG = 163 273 | KEY_PLAYPAUSE = 164 274 | KEY_PREVIOUSSONG = 165 275 | KEY_STOPCD = 166 276 | KEY_RECORD = 167 277 | KEY_REWIND = 168 278 | KEY_PHONE = 169 --[ Media Select Telephone ]-- 279 | KEY_ISO = 170 280 | KEY_CONFIG = 171 --[ AL Consumer Control Configuration ]-- 281 | KEY_HOMEPAGE = 172 --[ AC Home ]-- 282 | KEY_REFRESH = 173 --[ AC Refresh ]-- 283 | KEY_EXIT = 174 --[ AC Exit ]-- 284 | KEY_MOVE = 175 285 | KEY_EDIT = 176 286 | KEY_SCROLLUP = 177 287 | KEY_SCROLLDOWN = 178 288 | KEY_KPLEFTPAREN = 179 289 | KEY_KPRIGHTPAREN = 180 290 | KEY_NEW = 181 --[ AC New ]-- 291 | KEY_REDO = 182 --[ AC Redo/Repeat ]-- 292 | 293 | KEY_F13 = 183 294 | KEY_F14 = 184 295 | KEY_F15 = 185 296 | KEY_F16 = 186 297 | KEY_F17 = 187 298 | KEY_F18 = 188 299 | KEY_F19 = 189 300 | KEY_F20 = 190 301 | KEY_F21 = 191 302 | KEY_F22 = 192 303 | KEY_F23 = 193 304 | KEY_F24 = 194 305 | 306 | BTN_GAMEPAD = 0x130 307 | BTN_SOUTH = 0x130 308 | BTN_A = BTN_SOUTH 309 | BTN_EAST = 0x131 310 | BTN_B = BTN_EAST 311 | BTN_C = 0x132 312 | BTN_NORTH = 0x133 313 | BTN_X = BTN_NORTH 314 | BTN_WEST = 0x134 315 | BTN_Y = BTN_WEST 316 | BTN_Z = 0x135 317 | BTN_TL = 0x136 318 | BTN_TR = 0x137 319 | BTN_TL2 = 0x138 320 | BTN_TR2 = 0x139 321 | BTN_SELECT = 0x13a 322 | BTN_START = 0x13b 323 | BTN_MODE = 0x13c 324 | BTN_THUMBL = 0x13d 325 | BTN_THUMBR = 0x13e 326 | 327 | 328 | --[[ DENON DN-SC2000 ]]-- 329 | ------------------------- 330 | controller = {} 331 | 332 | --The controller can be configure to use different channels for decks A and B 333 | -- consult the official instructions on how to change the channels of the 334 | -- controller. and then change these variables to reflect 335 | controller.DECKA = { channel = 0x00 } 336 | controller.DECKB = { channel = 0x01 } 337 | 338 | --[[ Controller Codes ]]-- 339 | -- All of the Note On codes for the controller 340 | -- n = MIDI CH = 0-7 341 | -- packet { 342 | -- SW ON: 0x9n SW OFF: 0x8n, 343 | -- , 344 | -- SW ON: 0x40 SW OFF: 0x00 345 | -- } 346 | XC_AUTO_LOOP = 0x1D 347 | XC_BEAT_MINUS = 0x69 348 | XC_BEAT_PLUS = 0x6A 349 | XC_BEND_MINUS = 0x0D 350 | XC_BEND_PLUS = 0x0C 351 | XC_BROWSER = 0x64 352 | XC_CUE = 0x42 353 | XC_CUE1 = 0x17 354 | XC_CUE2 = 0x18 355 | XC_CUE3 = 0x19 356 | XC_CUE4 = 0x20 357 | XC_CUE5 = 0x21 358 | XC_CUE6 = 0x22 359 | XC_CUE7 = 0x23 360 | XC_CUE8 = 0x24 361 | XC_CUE_LOOP_DELETE_SHIFT = 0x60 362 | XC_DECK_CHG = 0x03 363 | XC_DRY_WET_SW_FX1 = 0x15 364 | XC_DRY_WET_SW_FX2 = 0x55 365 | XC_DUPLICATE = 0x65 366 | XC_FAST_SEARCH_MINUS = 0x11 367 | XC_FAST_SEARCH_PLUS = 0x10 368 | XC_FILTER_SW = 0x68 369 | XC_FX_ON_1 = 0x56 370 | XC_FX_ON_2 = 0x57 371 | XC_FX_UNIT = 0x58 372 | XC_JOGWHEEL_TOUCH = 0x51 373 | XC_KEY_LOCK = 0x06 374 | XC_LOOP_IN = 0x37 375 | XC_LOOP_OUT = 0x39 376 | XC_NEXT = 0x29 377 | XC_PARAM1_SW_FX1 = 0x12 378 | XC_PARAM1_SW_FX2 = 0x52 379 | XC_PARAM2_SW_FX1 = 0x13 380 | XC_PARAM2_SW_FX2 = 0x53 381 | XC_PARAM3_SW_FX1 = 0x14 382 | XC_PARAM3_SW_FX2 = 0x54 383 | XC_PLAY = 0x43 384 | XC_PREVIOUS = 0x30 385 | XC_SEL = 0x1A 386 | XC_SYNC = 0x6B 387 | XC_TRACK_SELECT_SW = 0x28 388 | 389 | -- Control Signal codes 390 | -- packet { 391 | -- 0xBn, 392 | -- , 393 | -- Increment: 00 394 | -- Decrement: 0x7F 395 | -- } 396 | XC_DRY_WET_KNOB_FX1 = 0x55 397 | XC_DRY_WET_KNOB_FX2 = 0x59 398 | XC_PARAM1_KNOB_FX1 = 0x56 399 | XC_PARAM1_KNOB_FX2 = 0x5A 400 | XC_PARAM2_KNOB_FX1 = 0x57 401 | XC_PARAM2_KNOB_FX2 = 0x5B 402 | XC_PARAM3_KNOB_FX1 = 0x58 403 | XC_PARAM3_KNOB_FX2 = 0x5C 404 | XC_TRACK_SELECT_KNOB = 0x54 405 | XC_FILTER_KNOB = 0x5D 406 | 407 | -- packet { 408 | -- 0xBn, 409 | -- , 410 | -- Reverse: 0x3F-0x01 411 | -- Forward: 0x41~0x7F 412 | -- Slow->Fast 413 | -- * relative data 2048Pulse/cycle 414 | -- } 415 | XC_JOGWHEEL_FWD_REV = 0x51 416 | 417 | --PITCH_SLIDER is a bit different, with packets like this 418 | --{0xEn, 0xllx(LSB), 0xmm (MSB) } 419 | 420 | --The lights are controlled by sending a control signal with the first byte 421 | --indicating the signal to send, and the second byte indicating the light 422 | -- signals: 423 | -- * 0x4A = On 424 | -- * 0x4B = Off 425 | -- * 0x4C = Blink 426 | -- ie. {0xBn, , }, where n is the channel number 427 | -- eg. {0xB0, 0x4A, 0x5C} - switches the play light on. 428 | -- Some buttons have two light values associated with them, one light is normal 429 | -- brightness, and the other is a dim version, both the normal and the dim 430 | -- light use the same control signals of on, off, blink. 431 | 432 | -- Confusingly the control signals to change the light value have no 433 | -- relationship with the note on and off values that are sent by the controller 434 | -- which makes sense only in the case of the normal and dim light buttons, 435 | -- because there are two lights for the same button. 436 | XL_DRY_WET_KNOB_FX1 = 0x5C 437 | XL_DRY_WET_KNOB_FX2 = 0x60 438 | XL_PARAM_1_KNOB_FX1 = 0x5D 439 | XL_PARAM_1_KNOB_FX2 = 0x61 440 | XL_PARAM_2_KNOB_FX1 = 0x5E 441 | XL_PARAM_2_KNOB_FX2 = 0x62 442 | XL_PARAM_3_KNOB_FX1 = 0x5F 443 | XL_PARAM_3_KNOB_FX2 = 0x63 444 | XL_FX_ON_1 = 0x5A 445 | XL_FX_ON_2 = 0x5B 446 | XL_KEY_LOCK = 0x08 447 | XL_SYNC = 0x09 448 | XL_CUE1 = 0x11 449 | XL_CUE1_Dimmer = 0x12 450 | XL_CUE2 = 0x13 451 | XL_CUE2_Dimmer = 0x14 452 | XL_CUE3 = 0x15 453 | XL_CUE3_Dimmer = 0x16 454 | XL_CUE4 = 0x17 455 | XL_CUE4_Dimmer = 0x18 456 | XL_CUE5 = 0x19 457 | XL_CUE5_Dimmer = 0x1A 458 | XL_CUE6 = 0x1B 459 | XL_CUE6_Dimmer = 0x1C 460 | XL_CUE7 = 0x1D 461 | XL_CUE7_Dimmer = 0x1F 462 | XL_CUE8 = 0x20 463 | XL_CUE8_Dimmer = 0x21 464 | XL_PLAY = 0x27 465 | XL_CUE = 0x26 466 | XL_LOOP_IN = 0x24 467 | XL_LOOP_IN_Dimmer = 0x3E 468 | XL_LOOP_OUT = 0x40 469 | XL_LOOP_OUT_Dimmer = 0x2A 470 | XL_AUTO_LOOP = 0x2B 471 | XL_AUTO_LOOP_Dimmer = 0x53 472 | 473 | 474 | --[[ Application Functions ]]-- 475 | --[[=======================]]-- 476 | 477 | -- use deck a as scroll wheel 478 | jogdial = { 479 | counter = 0, 480 | turn = function( event ) 481 | if( jogdial.counter < 10 ) then 482 | jogdial.counter = jogdial.counter + 1 483 | return 484 | else 485 | jogdial.counter = 0 486 | end 487 | 488 | if event[3] < 64 then 489 | mousescroll( 120 ) 490 | end 491 | if event[3] > 64 then 492 | mousescroll( -120 ) 493 | end 494 | end 495 | } 496 | 497 | --[[ Clementine ]]-- 498 | clementine = {} 499 | clementine.next_prev = function( event ) 500 | if( event[3] > 65 ) then 501 | exec( "clementine -r" ) 502 | else 503 | exec( "clementine -f" ) 504 | end 505 | end 506 | 507 | clementine.play_pause = function( event ) 508 | exec( "clementine -t" ) 509 | end 510 | 511 | --[[ Pulse Audio ]]-- 512 | pa = { 513 | volume = 0, 514 | volume_ms = milliseconds(), 515 | set_volume = function( event ) 516 | if( milliseconds() - pa.volume_ms < 25 )then 517 | return 518 | end 519 | pa.volume_ms = milliseconds() 520 | 521 | temp = math.floor( (event[3] / 2.550) ) 522 | if( pa.volume == temp) then 523 | return 524 | end 525 | pa.volume = temp 526 | -- old way exec("amixer -D pulse sset Master " .. temp .. "%" ) 527 | exec( "pactl set-sink-volume @DEFAULT_SINK@ " .. temp .. "%" ) 528 | end 529 | } 530 | 531 | --[[ Default Keymap ]]-- 532 | ------------------------ 533 | default = {} 534 | 535 | default.map = {} 536 | 537 | --DECK A:BLUE Note-On functions 538 | default.map[0x90 + controller.DECKA.channel ]= { 539 | [ XC_PLAY ] = { ['*'] = clementine.play_pause }, 540 | [ XC_CUE ] = { 541 | [ 64 ] = function( event ) keydown( BTN_A ) end, 542 | [ 0 ] = function( event ) keyup( BTN_A ) end 543 | }, 544 | } 545 | 546 | --DECK A:BLUE control signals 547 | default.map[0xB0 + controller.DECKA.channel ] = { 548 | [ XC_TRACK_SELECT_KNOB ] = { ['*'] = clementine.next_prev }, 549 | [ XC_JOGWHEEL_FWD_REV ] = { ['*'] = jogdial.turn }, 550 | } 551 | 552 | --DECK A:BLUE Pitch Bend 553 | default.map[0xE0 + controller.DECKA.channel] = { 554 | [0x00] = { ['*'] = pa.set_volume }, 555 | } 556 | 557 | --[[ application specific maps ]]-- 558 | --[[===========================]]-- 559 | 560 | --[[ ffplay ]]-- 561 | ffplay = {} 562 | ffplay.map = {} 563 | 564 | -- play pause when play button is pressed 565 | ffplay.map[ 0x90 + controller.DECKB.channel ]= { 566 | [ XC_PLAY ]= { [127]= function( event ) 567 | keypress( XK_space ) 568 | end }, 569 | } 570 | -- scrub through footage with jog wheel 571 | ffplay.map[0xB0 + controller.DECKB.channel ]= { 572 | [ XC_JOGWHEEL_FWD_REV ] = { ['*'] = 573 | function( event ) 574 | if event[3] > 64 then 575 | keypress( XK_Right ) 576 | end 577 | if event[3] < 64 then 578 | keypress( XK_Left ) 579 | end 580 | end }, 581 | } 582 | 583 | --[[ Blender ]]-- 584 | --[[ ======= ]]-- 585 | 586 | Blender = {} 587 | Blender.func = {} 588 | 589 | function Blender.func.scrub( event ) 590 | if event[3] > 64 then 591 | keypress( XK_Right ) 592 | end 593 | if event[3] < 64 then 594 | keypress( XK_Left ) 595 | end 596 | end 597 | 598 | Blender.map = {} 599 | 600 | Blender.map[0x90 + controller.DECKB.channel ] = { 601 | [ XC_CUE ] = { ['*'] = function( event ) keypress( XK_m ) end }, 602 | [ XC_PLAY ] = { ['*'] = function( event ) keypress( XK_space ) end }, 603 | [ XC_NEXT ] = { ['*'] = function( event ) keypress( XK_Page_Up ) end }, 604 | [ XC_PREVIOUS ] = { ['*'] = function( event ) keypress( XK_Page_Down ) end }, 605 | [ XC_FAST_SEARCH_PLUS ] = { ['*'] = 606 | function( event ) kpress( event, { XK_Shift_L, XK_period } ) end }, 607 | [ XC_FAST_SEARCH_MINUS ] = { ['*'] = 608 | function( event ) kpress( event, { XK_Shift_L, XK_comma }) end }, 609 | } 610 | Blender.map[0xB0 + controller.DECKB.channel ] = { 611 | [ XC_JOGWHEEL_FWD_REV ] = { ['*'] = Blender.func.scrub }, 612 | [ XC_TRACK_SELECT_KNOB ] = { 613 | [ 0] = function( event ) keypress( XK_Page_Up ) end, 614 | [127] = function( event ) keypress( XK_Page_Down ) end 615 | }, 616 | } 617 | 618 | --[[ Built in midi2input control functions ]]-- 619 | --[[=======================================]]-- 620 | function script_init() 621 | print( "beans nothing to do" ) 622 | loopenable() 623 | end 624 | 625 | function loop() 626 | detectwindow(); 627 | --alsaconnect( "DN-SC2000", "*" ); 628 | return 0; 629 | end 630 | 631 | --[[ receive and react ]]-- 632 | function midi_recv( channel, control, value ) 633 | event = {channel, control, value} 634 | local app = _G[ WM_CLASS ] 635 | if not app then app = default end 636 | 637 | local current = app.map 638 | --check for channel and control first. 639 | if current[channel] and current[channel][control] 640 | then 641 | local control = current[channel][control] 642 | -- we have the control, now look for the value or wildcard 643 | if control[ value ] then 644 | control[ value ]( event ) 645 | elseif control[ '*' ] then 646 | control[ '*' ]( event ) 647 | else 648 | --unable to find action for event 649 | return 650 | end 651 | end 652 | end 653 | 654 | 655 | -------------------------------------------------------------------------------- /cfg/ddj-wego.lua: -------------------------------------------------------------------------------- 1 | --[[ Defines ]]-- 2 | -- these are all taken from X11\keysymdef.h 3 | XK_BackSpace = 0xff08 --/* Back space, back char */ 4 | XK_Tab = 0xff09 5 | XK_Linefeed = 0xff0a --/* Linefeed, LF */ 6 | XK_Clear = 0xff0b 7 | XK_Return = 0xff0d --/* Return, enter */ 8 | XK_Pause = 0xff13 --/* Pause, hold */ 9 | XK_Scroll_Lock = 0xff14 10 | XK_Sys_Req = 0xff15 11 | XK_Escape = 0xff1b 12 | XK_Delete = 0xffff --/* Delete, rubout */ 13 | XK_Home = 0xff50 14 | XK_Left = 0xff51 --/* Move left, left arrow */ 15 | XK_Up = 0xff52 --/* Move up, up arrow */ 16 | XK_Right = 0xff53 --/* Move right, right arrow */ 17 | XK_Down = 0xff54 --/* Move down, down arrow */ 18 | XK_Prior = 0xff55 --/* Prior, previous */ 19 | XK_Page_Up = 0xff55 20 | XK_Next = 0xff56 --/* Next */ 21 | XK_Page_Down = 0xff56 22 | XK_End = 0xff57 --/* EOL */ 23 | XK_Begin = 0xff58 --/* BOL */ 24 | XK_F1 = 0xffbe 25 | XK_F2 = 0xffbf 26 | XK_F3 = 0xffc0 27 | XK_F4 = 0xffc1 28 | XK_F5 = 0xffc2 29 | XK_F6 = 0xffc3 30 | XK_F7 = 0xffc4 31 | XK_F8 = 0xffc5 32 | XK_F9 = 0xffc6 33 | XK_F10 = 0xffc7 34 | XK_F11 = 0xffc8 35 | XK_L1 = 0xffc8 36 | XK_F12 = 0xffc9 37 | XK_L2 = 0xffc9 38 | XK_F13 = 0xffca 39 | XK_L3 = 0xffca 40 | XK_F14 = 0xffcb 41 | XK_L4 = 0xffcb 42 | XK_F15 = 0xffcc 43 | XK_L5 = 0xffcc 44 | XK_F16 = 0xffcd 45 | XK_L6 = 0xffcd 46 | XK_F17 = 0xffce 47 | XK_L7 = 0xffce 48 | XK_F18 = 0xffcf 49 | XK_L8 = 0xffcf 50 | XK_F19 = 0xffd0 51 | XK_L9 = 0xffd0 52 | XK_F20 = 0xffd1 53 | XK_L10 = 0xffd1 54 | XK_F21 = 0xffd2 55 | XK_R1 = 0xffd2 56 | XK_F22 = 0xffd3 57 | XK_R2 = 0xffd3 58 | XK_F23 = 0xffd4 59 | XK_R3 = 0xffd4 60 | XK_F24 = 0xffd5 61 | XK_R4 = 0xffd5 62 | XK_F25 = 0xffd6 63 | XK_R5 = 0xffd6 64 | XK_F26 = 0xffd7 65 | XK_R6 = 0xffd7 66 | XK_F27 = 0xffd8 67 | XK_R7 = 0xffd8 68 | XK_F28 = 0xffd9 69 | XK_R8 = 0xffd9 70 | XK_F29 = 0xffda 71 | XK_R9 = 0xffda 72 | XK_F30 = 0xffdb 73 | XK_R10 = 0xffdb 74 | XK_F31 = 0xffdc 75 | XK_R11 = 0xffdc 76 | XK_F32 = 0xffdd 77 | XK_R12 = 0xffdd 78 | XK_F33 = 0xffde 79 | XK_R13 = 0xffde 80 | XK_F34 = 0xffdf 81 | XK_R14 = 0xffdf 82 | XK_F35 = 0xffe0 83 | XK_R15 = 0xffe0 84 | XK_Shift_L = 0xffe1 --/* Left shift */ 85 | XK_Shift_R = 0xffe2 --/* Right shift */ 86 | XK_Control_L = 0xffe3 --/* Left control */ 87 | XK_Control_R = 0xffe4 --/* Right control */ 88 | XK_Caps_Lock = 0xffe5 --/* Caps lock */ 89 | XK_Shift_Lock = 0xffe6 --/* Shift lock */ 90 | XK_Meta_L = 0xffe7 --/* Left meta */ 91 | XK_Meta_R = 0xffe8 --/* Right meta */ 92 | XK_Alt_L = 0xffe9 --/* Left alt */ 93 | XK_Alt_R = 0xffea --/* Right alt */ 94 | XK_Super_L = 0xffeb --/* Left super */ 95 | XK_Super_R = 0xffec --/* Right super */ 96 | XK_Hyper_L = 0xffed --/* Left hyper */ 97 | XK_Hyper_R = 0xffee --/* Right hyper */ 98 | XK_space = 0x0020 --/* U+0020 SPACE */ 99 | XK_exclam = 0x0021 --/* U+0021 EXCLAMATION MARK */ 100 | XK_quotedbl = 0x0022 --/* U+0022 QUOTATION MARK */ 101 | XK_numbersign = 0x0023 --/* U+0023 NUMBER SIGN */ 102 | XK_dollar = 0x0024 --/* U+0024 DOLLAR SIGN */ 103 | XK_percent = 0x0025 --/* U+0025 PERCENT SIGN */ 104 | XK_ampersand = 0x0026 --/* U+0026 AMPERSAND */ 105 | XK_apostrophe = 0x0027 --/* U+0027 APOSTROPHE */ 106 | XK_quoteright = 0x0027 --/* deprecated */ 107 | XK_parenleft = 0x0028 --/* U+0028 LEFT PARENTHESIS */ 108 | XK_parenright = 0x0029 --/* U+0029 RIGHT PARENTHESIS */ 109 | XK_asterisk = 0x002a --/* U+002A ASTERISK */ 110 | XK_plus = 0x002b --/* U+002B PLUS SIGN */ 111 | XK_comma = 0x002c --/* U+002C COMMA */ 112 | XK_minus = 0x002d --/* U+002D HYPHEN-MINUS */ 113 | XK_period = 0x002e --/* U+002E FULL STOP */ 114 | XK_slash = 0x002f --/* U+002F SOLIDUS */ 115 | XK_0 = 0x0030 --/* U+0030 DIGIT ZERO */ 116 | XK_1 = 0x0031 --/* U+0031 DIGIT ONE */ 117 | XK_2 = 0x0032 --/* U+0032 DIGIT TWO */ 118 | XK_3 = 0x0033 --/* U+0033 DIGIT THREE */ 119 | XK_4 = 0x0034 --/* U+0034 DIGIT FOUR */ 120 | XK_5 = 0x0035 --/* U+0035 DIGIT FIVE */ 121 | XK_6 = 0x0036 --/* U+0036 DIGIT SIX */ 122 | XK_7 = 0x0037 --/* U+0037 DIGIT SEVEN */ 123 | XK_8 = 0x0038 --/* U+0038 DIGIT EIGHT */ 124 | XK_9 = 0x0039 --/* U+0039 DIGIT NINE */ 125 | XK_colon = 0x003a --/* U+003A COLON */ 126 | XK_semicolon = 0x003b --/* U+003B SEMICOLON */ 127 | XK_less = 0x003c --/* U+003C LESS-THAN SIGN */ 128 | XK_equal = 0x003d --/* U+003D EQUALS SIGN */ 129 | XK_greater = 0x003e --/* U+003E GREATER-THAN SIGN */ 130 | XK_question = 0x003f --/* U+003F QUESTION MARK */ 131 | XK_at = 0x0040 --/* U+0040 COMMERCIAL AT */ 132 | XK_A = 0x0041 --/* U+0041 LATIN CAPITAL LETTER A */ 133 | XK_B = 0x0042 --/* U+0042 LATIN CAPITAL LETTER B */ 134 | XK_C = 0x0043 --/* U+0043 LATIN CAPITAL LETTER C */ 135 | XK_D = 0x0044 --/* U+0044 LATIN CAPITAL LETTER D */ 136 | XK_E = 0x0045 --/* U+0045 LATIN CAPITAL LETTER E */ 137 | XK_F = 0x0046 --/* U+0046 LATIN CAPITAL LETTER F */ 138 | XK_G = 0x0047 --/* U+0047 LATIN CAPITAL LETTER G */ 139 | XK_H = 0x0048 --/* U+0048 LATIN CAPITAL LETTER H */ 140 | XK_I = 0x0049 --/* U+0049 LATIN CAPITAL LETTER I */ 141 | XK_J = 0x004a --/* U+004A LATIN CAPITAL LETTER J */ 142 | XK_K = 0x004b --/* U+004B LATIN CAPITAL LETTER K */ 143 | XK_L = 0x004c --/* U+004C LATIN CAPITAL LETTER L */ 144 | XK_Shift_L = 0xffe1 --/* Left shift */ 145 | XK_Shift_R = 0xffe2 --/* Right shift */ 146 | XK_Control_L = 0xffe3 --/* Left control */ 147 | XK_Control_R = 0xffe4 --/* Right control */ 148 | XK_Caps_Lock = 0xffe5 --/* Caps lock */ 149 | XK_Shift_Lock = 0xffe6 --/* Shift lock */ 150 | XK_Meta_L = 0xffe7 --/* Left meta */ 151 | XK_Meta_R = 0xffe8 --/* Right meta */ 152 | XK_Alt_L = 0xffe9 --/* Left alt */ 153 | XK_Alt_R = 0xffea --/* Right alt */ 154 | XK_Super_L = 0xffeb --/* Left super */ 155 | XK_Super_R = 0xffec --/* Right super */ 156 | XK_Hyper_L = 0xffed --/* Left hyper */ 157 | XK_Hyper_R = 0xffee --/* Right hyper */ 158 | XK_space = 0x0020 --/* U+0020 SPACE */ 159 | XK_exclam = 0x0021 --/* U+0021 EXCLAMATION MARK */ 160 | XK_quotedbl = 0x0022 --/* U+0022 QUOTATION MARK */ 161 | XK_numbersign = 0x0023 --/* U+0023 NUMBER SIGN */ 162 | XK_dollar = 0x0024 --/* U+0024 DOLLAR SIGN */ 163 | XK_percent = 0x0025 --/* U+0025 PERCENT SIGN */ 164 | XK_ampersand = 0x0026 --/* U+0026 AMPERSAND */ 165 | XK_apostrophe = 0x0027 --/* U+0027 APOSTROPHE */ 166 | XK_quoteright = 0x0027 --/* deprecated */ 167 | XK_parenleft = 0x0028 --/* U+0028 LEFT PARENTHESIS */ 168 | XK_parenright = 0x0029 --/* U+0029 RIGHT PARENTHESIS */ 169 | XK_asterisk = 0x002a --/* U+002A ASTERISK */ 170 | XK_plus = 0x002b --/* U+002B PLUS SIGN */ 171 | XK_comma = 0x002c --/* U+002C COMMA */ 172 | XK_minus = 0x002d --/* U+002D HYPHEN-MINUS */ 173 | XK_period = 0x002e --/* U+002E FULL STOP */ 174 | XK_slash = 0x002f --/* U+002F SOLIDUS */ 175 | XK_0 = 0x0030 --/* U+0030 DIGIT ZERO */ 176 | XK_1 = 0x0031 --/* U+0031 DIGIT ONE */ 177 | XK_2 = 0x0032 --/* U+0032 DIGIT TWO */ 178 | XK_3 = 0x0033 --/* U+0033 DIGIT THREE */ 179 | XK_4 = 0x0034 --/* U+0034 DIGIT FOUR */ 180 | XK_5 = 0x0035 --/* U+0035 DIGIT FIVE */ 181 | XK_6 = 0x0036 --/* U+0036 DIGIT SIX */ 182 | XK_7 = 0x0037 --/* U+0037 DIGIT SEVEN */ 183 | XK_8 = 0x0038 --/* U+0038 DIGIT EIGHT */ 184 | XK_9 = 0x0039 --/* U+0039 DIGIT NINE */ 185 | XK_colon = 0x003a --/* U+003A COLON */ 186 | XK_semicolon = 0x003b --/* U+003B SEMICOLON */ 187 | XK_less = 0x003c --/* U+003C LESS-THAN SIGN */ 188 | XK_equal = 0x003d --/* U+003D EQUALS SIGN */ 189 | XK_greater = 0x003e --/* U+003E GREATER-THAN SIGN */ 190 | XK_question = 0x003f --/* U+003F QUESTION MARK */ 191 | XK_at = 0x0040 --/* U+0040 COMMERCIAL AT */ 192 | XK_A = 0x0041 --/* U+0041 LATIN CAPITAL LETTER A */ 193 | XK_B = 0x0042 --/* U+0042 LATIN CAPITAL LETTER B */ 194 | XK_C = 0x0043 --/* U+0043 LATIN CAPITAL LETTER C */ 195 | XK_D = 0x0044 --/* U+0044 LATIN CAPITAL LETTER D */ 196 | XK_E = 0x0045 --/* U+0045 LATIN CAPITAL LETTER E */ 197 | XK_F = 0x0046 --/* U+0046 LATIN CAPITAL LETTER F */ 198 | XK_G = 0x0047 --/* U+0047 LATIN CAPITAL LETTER G */ 199 | XK_H = 0x0048 --/* U+0048 LATIN CAPITAL LETTER H */ 200 | XK_I = 0x0049 --/* U+0049 LATIN CAPITAL LETTER I */ 201 | XK_J = 0x004a --/* U+004A LATIN CAPITAL LETTER J */ 202 | XK_K = 0x004b --/* U+004B LATIN CAPITAL LETTER K */ 203 | XK_L = 0x004c --/* U+004C LATIN CAPITAL LETTER L */ 204 | XK_M = 0x004d --/* U+004D LATIN CAPITAL LETTER M */ 205 | XK_N = 0x004e --/* U+004E LATIN CAPITAL LETTER N */ 206 | XK_O = 0x004f --/* U+004F LATIN CAPITAL LETTER O */ 207 | XK_P = 0x0050 --/* U+0050 LATIN CAPITAL LETTER P */ 208 | XK_Q = 0x0051 --/* U+0051 LATIN CAPITAL LETTER Q */ 209 | XK_R = 0x0052 --/* U+0052 LATIN CAPITAL LETTER R */ 210 | XK_S = 0x0053 --/* U+0053 LATIN CAPITAL LETTER S */ 211 | XK_T = 0x0054 --/* U+0054 LATIN CAPITAL LETTER T */ 212 | XK_U = 0x0055 --/* U+0055 LATIN CAPITAL LETTER U */ 213 | XK_V = 0x0056 --/* U+0056 LATIN CAPITAL LETTER V */ 214 | XK_W = 0x0057 --/* U+0057 LATIN CAPITAL LETTER W */ 215 | XK_X = 0x0058 --/* U+0058 LATIN CAPITAL LETTER X */ 216 | XK_Y = 0x0059 --/* U+0059 LATIN CAPITAL LETTER Y */ 217 | XK_Z = 0x005a --/* U+005A LATIN CAPITAL LETTER Z */ 218 | XK_bracketleft = 0x005b --/* U+005B LEFT SQUARE BRACKET */ 219 | XK_backslash = 0x005c --/* U+005C REVERSE SOLIDUS */ 220 | XK_bracketright = 0x005d --/* U+005D RIGHT SQUARE BRACKET */ 221 | XK_asciicircum = 0x005e --/* U+005E CIRCUMFLEX ACCENT */ 222 | XK_underscore = 0x005f --/* U+005F LOW LINE */ 223 | XK_grave = 0x0060 --/* U+0060 GRAVE ACCENT */ 224 | XK_quoteleft = 0x0060 --/* deprecated */ 225 | XK_a = 0x0061 --/* U+0061 LATIN SMALL LETTER A */ 226 | XK_b = 0x0062 --/* U+0062 LATIN SMALL LETTER B */ 227 | XK_c = 0x0063 --/* U+0063 LATIN SMALL LETTER C */ 228 | XK_d = 0x0064 --/* U+0064 LATIN SMALL LETTER D */ 229 | XK_e = 0x0065 --/* U+0065 LATIN SMALL LETTER E */ 230 | XK_f = 0x0066 --/* U+0066 LATIN SMALL LETTER F */ 231 | XK_g = 0x0067 --/* U+0067 LATIN SMALL LETTER G */ 232 | XK_h = 0x0068 --/* U+0068 LATIN SMALL LETTER H */ 233 | XK_i = 0x0069 --/* U+0069 LATIN SMALL LETTER I */ 234 | XK_j = 0x006a --/* U+006A LATIN SMALL LETTER J */ 235 | XK_k = 0x006b --/* U+006B LATIN SMALL LETTER K */ 236 | XK_l = 0x006c --/* U+006C LATIN SMALL LETTER L */ 237 | XK_m = 0x006d --/* U+006D LATIN SMALL LETTER M */ 238 | XK_n = 0x006e --/* U+006E LATIN SMALL LETTER N */ 239 | XK_o = 0x006f --/* U+006F LATIN SMALL LETTER O */ 240 | XK_p = 0x0070 --/* U+0070 LATIN SMALL LETTER P */ 241 | XK_q = 0x0071 --/* U+0071 LATIN SMALL LETTER Q */ 242 | XK_r = 0x0072 --/* U+0072 LATIN SMALL LETTER R */ 243 | XK_s = 0x0073 --/* U+0073 LATIN SMALL LETTER S */ 244 | XK_t = 0x0074 --/* U+0074 LATIN SMALL LETTER T */ 245 | XK_u = 0x0075 --/* U+0075 LATIN SMALL LETTER U */ 246 | XK_v = 0x0076 --/* U+0076 LATIN SMALL LETTER V */ 247 | XK_w = 0x0077 --/* U+0077 LATIN SMALL LETTER W */ 248 | XK_x = 0x0078 --/* U+0078 LATIN SMALL LETTER X */ 249 | XK_y = 0x0079 --/* U+0079 LATIN SMALL LETTER Y */ 250 | XK_z = 0x007a --/* U+007A LATIN SMALL LETTER Z */ 251 | XK_braceleft = 0x007b --/* U+007B LEFT CURLY BRACKET */ 252 | XK_bar = 0x007c --/* U+007C VERTICAL LINE */ 253 | XK_braceright = 0x007d --/* U+007D RIGHT CURLY BRACKET */ 254 | XK_asciitilde = 0x007e --/* U+007E TILDE */ 255 | 256 | --[[ global settings ]]-- 257 | -- autoconnect: can be true, false, or a named jack port. default = true 258 | autoconnect = true 259 | 260 | -- a funtion to recursively walk the ddj structure and send signals 261 | -- to the controller to refresh the lighting state. 262 | function refresh_state( object ) 263 | for index,value in pairs( object ) do 264 | if( type( object[ index ] ) == "table" ) then 265 | refresh_state( object[ index ] ) 266 | end 267 | if( index == "lstate" ) then 268 | if( value == true ) then 269 | midi_send( object.lon ) 270 | else 271 | midi_send( object.loff ) 272 | end 273 | 274 | -- if we send all the events at once the controller does not 275 | -- respond to them all. 0.001 failed. 276 | sleep(0.01) 277 | end 278 | end 279 | end 280 | 281 | local clock = os.clock 282 | function sleep(n) -- seconds 283 | local t0 = clock() 284 | while clock() - t0 <= n do end 285 | end 286 | -- warning: clock can eventually wrap around for sufficiently large n 287 | -- (whose value is platform dependent). Even for n == 1, clock() - t0 288 | -- might become negative on the second that clock wraps. 289 | 290 | --[[ ddj-wego button to midi tables]]-- 291 | ddj = { 292 | misc = { 293 | browse = { 294 | down = { 0x96, 0x41, 127 }, 295 | up = { 0x86, 0x41, 64 }, 296 | turn = { 0xB6, 0x40, -1 }, -- values 0-30 or 98-127 297 | sdown = { 0x96, 0x42, 127 }, 298 | sup = { 0x86, 0x42, 64 }, 299 | sturn = { 0xB6, 0x64, -1 }, -- values 0-30 or 98-127 300 | }, 301 | hpmix = { 302 | msb = { 0xB6, 0x01 }, 303 | lsb = { 0xB6, 0x21 }, 304 | }, 305 | crossfader = { 306 | msb = { 0xB6, 0x1F }, 307 | lsb = { 0xB6, 0x3F }, 308 | smsb = { 0xB6, 0x00 }, 309 | slsb = { 0xB6, 0x20 }, 310 | }, 311 | deckC = { 312 | down = { 0x92, 0x72, 127 }, 313 | up = { 0x82, 0x72, 64 }, 314 | lon = { 0x92, 0x72, 127 }, 315 | loff = { 0x92, 0x72, 0 }, 316 | lstate = false, 317 | }, 318 | deckD = { 319 | down = { 0x93, 0x72, 127 }, 320 | up = { 0x83, 0x72, 64 }, 321 | lon = { 0x93, 0x72, 127 }, 322 | loff = { 0x93, 0x72, 0 }, 323 | lstate = false, 324 | }, 325 | }, 326 | } 327 | 328 | --[[ add the four decks substituting the channel ]]-- 329 | decks = { deckA = 1; deckB = 2; deckC = 3, deckD = 4 } 330 | for deck, ch in pairs(decks) do 331 | local note_on = 0x90 + ch -1 332 | local note_off = 0x80 + ch -1 333 | local control = 0xB0 + ch -1 334 | 335 | ddj[deck] = {} 336 | ddj[deck] = { 337 | tempo = { 338 | msb = { control, 0x00 }, 339 | lsb = { control, 0x20 }, 340 | smsb = { control, 0x05 }, 341 | slsb = { control, 0x25 }, 342 | }, 343 | sync = { 344 | down = { note_on, 0x58, 127 }, 345 | up = { note_off, 0x58, 64 }, 346 | sdown = { note_on, 0x5C, 127 }, 347 | sup = { note_off, 0x5C, 64 }, 348 | lon = { note_on, 0x58, 127 }, 349 | loff = { note_on, 0x58, 0 }, 350 | lstate = false, 351 | }, 352 | 353 | autoloop = { 354 | turn = { control, 0x13, -1 }, -- values 0-30 or 98-127 355 | sturn = { control, 0x4F, -1 }, -- values 0-30 or 98-127 356 | 357 | next = { control, 0x13, 1 }, 358 | snext = { control, 0x4F, 1 }, 359 | 360 | prev = { control, 0x13, 127 }, 361 | sprev = { control, 0x4F, 127 }, 362 | 363 | down = { note_on, 0x14, 127 }, 364 | sdown = { note_on, 0x50, 127 }, 365 | 366 | up = { note_off, 0x14, 64 }, 367 | sup = { note_off, 0x50, 64 }, 368 | }, 369 | load = { 370 | down = { 0x96, 0x45+ch, 127 }, 371 | up = { 0x86, 0x45+ch, 64 }, 372 | sdown = { 0x96, 0x57+ch, 127 }, 373 | sup = { 0x86, 0x57+ch, 64 }, 374 | }, 375 | 376 | --equaliser 377 | eqhi = { 378 | msb = { 0xB6, 0x07 }, 379 | lsb = { 0xB6, 0x27 }, 380 | }, 381 | eqmid = { 382 | msb = { 0xB6, 0x0B }, 383 | lsb = { 0xB6, 0x2B }, 384 | }, 385 | eqlow = { 386 | msb = { 0xB6, 0x0F }, 387 | lsb = { 0xB6, 0x2F }, 388 | }, 389 | 390 | headphone = { 391 | down = { 0x96, 0x53+ch, 127 }, 392 | up = { 0x86, 0x53+ch, 64 }, 393 | sdown = { 0x96, 0x5B+ch, 127 }, 394 | sup = { 0x86, 0x5B+ch, 64 }, 395 | lon = { 0x96, 0x53+ch, 127 }, 396 | loff = { 0x96, 0x53+ch, 0 }, 397 | lstate = false, 398 | }, 399 | fader = { 400 | msb = { 0xB6, 0x13 }, 401 | lsb = { 0xB6, 0x33 }, 402 | smsb = { 0xB6, 0x14 }, 403 | slsb = { 0xB6, 0x34 }, 404 | }, 405 | 406 | 407 | 408 | jogdial = { 409 | down = { note_on, 0x36, 127 }, 410 | up = { note_off, 0x36, 64 }, 411 | sdown = { note_on, 0x67, 127 }, 412 | sup = { note_on, 0x67, 64 }, 413 | turn = { control, 0x22, -1 }, --clockwise=65-127,cc=1-63 414 | sturn = { control, 0x27, -1 }, --clockwise=65-127,cc=1-63 415 | rim = { control, 0x22, -1 }, --clockwise=65-127,cc=1-63 416 | srim = { control, 0x26, -1 }, --clockwise=65-127,cc=1-63 417 | }, 418 | 419 | cue = { 420 | down = { note_on, 0x0C, 127 }, 421 | up = { note_off, 0x0C, 64 }, 422 | sdown = { note_on, 0x48, 127 }, 423 | sup = { note_off, 0x48, 64 }, 424 | lon = { note_on, 0x0C, 127 }, 425 | loff = { note_on, 0x0C, 0 }, 426 | lstate = false, 427 | }, 428 | play = { 429 | down = { note_on, 0x0B, 127 }, 430 | up = { note_off, 0x0B, 64 }, 431 | sdown = { note_on, 0x47, 127 }, 432 | sup = { note_off, 0x47, 64 }, 433 | lon = { note_on, 0x0B, 127 }, 434 | loff = { note_on, 0x0B, 0 }, 435 | lstate = false, 436 | }, 437 | 438 | --Samplers 439 | sampler = { 440 | down = { note_on, 0x59, 127 }, 441 | up = { note_off, 0x59, 64 }, 442 | }, 443 | sampler1 = { 444 | down = { note_on, 0x2E, 127 }, 445 | up = { note_off, 0x2E, 64 }, 446 | sdown = { note_on, 0x5F, 127 }, 447 | sup = { note_off, 0x5F, 64 }, 448 | lon = { note_on, 0x2E, 127 }, 449 | loff = { note_on, 0x2E, 0 }, 450 | lstate = false, 451 | }, 452 | sampler2 = { 453 | down = { note_on, 0x2F, 127 }, 454 | up = { note_off, 0x2F, 64 }, 455 | sdown = { note_on, 0x60, 127 }, 456 | sup = { note_off, 0x60, 64 }, 457 | lon = { note_on, 0x2F, 127 }, 458 | loff = { note_on, 0x2F, 0 }, 459 | lstate = false, 460 | }, 461 | sampler3 = { 462 | down = { note_on, 0x30, 127 }, 463 | up = { note_off, 0x30, 64 }, 464 | sdown = { note_on, 0x61, 127 }, 465 | sup = { note_off, 0x61, 64 }, 466 | lon = { note_on, 0x30, 127 }, 467 | loff = { note_on, 0x30, 0 }, 468 | lstate = false, 469 | }, 470 | sampler4 = { 471 | down = { note_on, 0x31, 127 }, 472 | up = { note_off, 0x31, 64 }, 473 | sdown = { note_on, 0x62, 127 }, 474 | sup = { note_off, 0x62, 64 }, 475 | lon = { note_on, 0x31, 127 }, 476 | loff = { note_on, 0x31, 0 }, 477 | lstate = false, 478 | }, 479 | sampler5 = { 480 | down = { note_on, 0x3C, 127 }, 481 | up = { note_off, 0x3C, 64 }, 482 | sdown = { note_on, 0x3D, 127 }, 483 | sup = { note_off, 0x3D, 64 }, 484 | lon = { note_on, 0x3C, 127 }, 485 | loff = { note_on, 0x3C, 0 }, 486 | lstate = false, 487 | }, 488 | sampler6 = { 489 | down = { note_on, 0x3E, 127 }, 490 | up = { note_off, 0x3E, 64 }, 491 | sdown = { note_on, 0x3F, 127 }, 492 | sup = { note_off, 0x3F, 64 }, 493 | lon = { note_on, 0x3E, 127 }, 494 | loff = { note_on, 0x3E, 0 }, 495 | lstate = false, 496 | }, 497 | sampler7 = { 498 | down = { note_on, 0x40, 127 }, 499 | up = { note_off, 0x40, 64 }, 500 | sdown = { note_on, 0x41, 127 }, 501 | sup = { note_off, 0x41, 64 }, 502 | lon = { note_on, 0x40, 127 }, 503 | loff = { note_on, 0x40, 0 }, 504 | lstate = false, 505 | }, 506 | sampler8 = { 507 | down = { note_on, 0x42, 127 }, 508 | up = { note_off, 0x42, 64 }, 509 | sdown = { note_on, 0x43, 127 }, 510 | sup = { note_off, 0x43, 64 }, 511 | lon = { note_on, 0x42, 127 }, 512 | loff = { note_on, 0x42, 0 }, 513 | lstate = false, 514 | }, 515 | 516 | 517 | 518 | } 519 | end 520 | -- Deck A 521 | ddj.deckA.ctrlA = { 522 | down = { 0x94, 0x42, 127 }, 523 | up = { 0x84, 0x42, 0 }, 524 | loff = { 0x94, 0x42, 0 }, 525 | lon = { 0x94, 0x42, 127 }, 526 | lstate = false, 527 | } 528 | ddj.deckA.FX1 = { 529 | down = { 0x94, 0x43, 127 }, 530 | up = { 0x84, 0x43, 64 }, 531 | sdown = { 0x94, 0x4D, 127 }, 532 | sup = { 0x94, 0x4D, 64 }, 533 | loff = { 0x94, 0x43, 0 }, 534 | lon = { 0x94, 0x43, 127 }, 535 | lstate = false, 536 | } 537 | ddj.deckA.FX2 = { 538 | down = { 0x94, 0x44, 127 }, 539 | up = { 0x84, 0x44, 64 }, 540 | sdown = { 0x94, 0x4E, 127 }, 541 | sup = { 0x94, 0x4E, 64 }, 542 | loff = { 0x94, 0x44, 0 }, 543 | lon = { 0x94, 0x44, 127 }, 544 | lstate = false, 545 | } 546 | ddj.deckA.FX3 = { 547 | down = { 0x94, 0x45, 127 }, 548 | up = { 0x84, 0x45, 64 }, 549 | sdown = { 0x94, 0x4F, 127 }, 550 | sup = { 0x94, 0x4F, 64 }, 551 | loff = { 0x94, 0x45, 0 }, 552 | lon = { 0x94, 0x45, 127 }, 553 | lstate = false, 554 | } 555 | ddj.deckA.ctrlB = { 556 | down = { 0x94, 0x46, 127 }, 557 | up = { 0x84, 0x46, 64 }, 558 | loff = { 0x94, 0x46, 0 }, 559 | lon = { 0x94, 0x46, 127 }, 560 | lstate = false, 561 | } 562 | -- Deck B 563 | ddj.deckB.ctrlA = { 564 | down = { 0x95, 0x42, 127 }, 565 | up = { 0x85, 0x42, 0 }, 566 | loff = { 0x95, 0x42, 0 }, 567 | lon = { 0x95, 0x42, 127 }, 568 | lstate = false, 569 | } 570 | ddj.deckB.FX1 = { 571 | down = { 0x95, 0x43, 127 }, 572 | up = { 0x85, 0x43, 64 }, 573 | sdown = { 0x95, 0x4D, 127 }, 574 | sup = { 0x95, 0x4D, 64 }, 575 | loff = { 0x95, 0x43, 0 }, 576 | lon = { 0x95, 0x43, 127 }, 577 | lstate = false, 578 | } 579 | ddj.deckB.FX2 = { 580 | down = { 0x95, 0x44, 127 }, 581 | up = { 0x85, 0x44, 64 }, 582 | sdown = { 0x95, 0x4E, 127 }, 583 | sup = { 0x95, 0x4E, 64 }, 584 | loff = { 0x95, 0x44, 0 }, 585 | lon = { 0x95, 0x44, 127 }, 586 | lstate = false, 587 | } 588 | ddj.deckB.FX3 = { 589 | down = { 0x95, 0x45, 127 }, 590 | up = { 0x85, 0x45, 64 }, 591 | sdown = { 0x95, 0x4F, 127 }, 592 | sup = { 0x95, 0x4F, 64 }, 593 | loff = { 0x95, 0x45, 0 }, 594 | lon = { 0x95, 0x45, 127 }, 595 | lstate = false, 596 | } 597 | ddj.deckB.ctrlB = { 598 | down = { 0x95, 0x46, 127 }, 599 | up = { 0x85, 0x46, 64 }, 600 | loff = { 0x95, 0x46, 0 }, 601 | lon = { 0x95, 0x46, 127 }, 602 | lstate = false, 603 | } 604 | 605 | -- Deck C 606 | ddj.deckC.ctrlA = { 607 | down = { 0x94, 0x47, 127 }, 608 | up = { 0x84, 0x47, 0 }, 609 | loff = { 0x94, 0x47, 0 }, 610 | lon = { 0x94, 0x47, 127 }, 611 | lstate = false, 612 | } 613 | ddj.deckC.FX1 = { 614 | down = { 0x94, 0x48, 127 }, 615 | up = { 0x84, 0x48, 64 }, 616 | sdown = { 0x94, 0x52, 127 }, 617 | sup = { 0x94, 0x52, 64 }, 618 | loff = { 0x94, 0x48, 0 }, 619 | lon = { 0x94, 0x48, 127 }, 620 | lstate = false, 621 | } 622 | ddj.deckC.FX2 = { 623 | down = { 0x94, 0x49, 127 }, 624 | up = { 0x84, 0x49, 64 }, 625 | sdown = { 0x94, 0x53, 127 }, 626 | sup = { 0x94, 0x53, 64 }, 627 | loff = { 0x94, 0x49, 0 }, 628 | lon = { 0x94, 0x49, 127 }, 629 | lstate = false, 630 | } 631 | ddj.deckC.FX3 = { 632 | down = { 0x94, 0x4A, 127 }, 633 | up = { 0x84, 0x4A, 64 }, 634 | sdown = { 0x94, 0x54, 127 }, 635 | sup = { 0x94, 0x54, 64 }, 636 | loff = { 0x94, 0x4A, 0 }, 637 | lon = { 0x94, 0x4A, 127 }, 638 | lstate = false, 639 | } 640 | ddj.deckC.ctrlB = { 641 | down = { 0x94, 0x4B, 127 }, 642 | up = { 0x84, 0x4B, 64 }, 643 | loff = { 0x94, 0x4B, 0 }, 644 | lon = { 0x94, 0x4B, 127 }, 645 | lstate = false, 646 | } 647 | --deck D 648 | ddj.deckD.ctrlA = { 649 | down = { 0x95, 0x47, 127 }, 650 | up = { 0x85, 0x47, 0 }, 651 | loff = { 0x95, 0x47, 0 }, 652 | lon = { 0x95, 0x47, 127 }, 653 | lstate = false, 654 | } 655 | ddj.deckD.FX1 = { 656 | down = { 0x95, 0x48, 127 }, 657 | up = { 0x85, 0x48, 64 }, 658 | sdown = { 0x95, 0x52, 127 }, 659 | sup = { 0x95, 0x52, 64 }, 660 | loff = { 0x95, 0x48, 0 }, 661 | lon = { 0x95, 0x48, 127 }, 662 | lstate = false, 663 | } 664 | ddj.deckD.FX2 = { 665 | down = { 0x95, 0x49, 127 }, 666 | up = { 0x85, 0x49, 64 }, 667 | sdown = { 0x95, 0x53, 127 }, 668 | sup = { 0x95, 0x53, 64 }, 669 | loff = { 0x95, 0x49, 0 }, 670 | lon = { 0x95, 0x49, 127 }, 671 | lstate = false, 672 | } 673 | ddj.deckD.FX3 = { 674 | down = { 0x95, 0x4A, 127 }, 675 | up = { 0x85, 0x4A, 64 }, 676 | sdown = { 0x95, 0x54, 127 }, 677 | sup = { 0x95, 0x54, 64 }, 678 | loff = { 0x95, 0x4A, 0 }, 679 | lon = { 0x95, 0x4A, 127 }, 680 | lstate = false, 681 | } 682 | ddj.deckD.ctrlB = { 683 | down = { 0x95, 0x4B, 127 }, 684 | up = { 0x85, 0x4B, 64 }, 685 | loff = { 0x95, 0x4B, 0 }, 686 | lon = { 0x95, 0x4B, 127 }, 687 | lstate = false, 688 | } 689 | -- generic test function to turn on and off lighted buttons 690 | function lbutton_toggle( button ) 691 | if button.lstate == false then 692 | button.lstate = true 693 | midi_send( button.lon ) 694 | else 695 | button.lstate = false 696 | midi_send( button.loff ) 697 | end 698 | end 699 | 700 | 701 | --[[ initialisation function ]]-- 702 | -- run immeditely after the application launches and connects to the device 703 | function initialise() 704 | print( "[LUA] nothing to initialise" ) 705 | --refresh_state( ddj ) 706 | return; 707 | end 708 | 709 | --[[ TODO add LOG( INFO/DEBUG/ERROR/ETC ) decription continues... 710 | -- logging functionality such that LOG(INFO) etc are available to lua. exposing the 'elog' logging level to lua 711 | -- probably will solve this nicely. 712 | ]] 713 | 714 | --[[ Action Tables ]]-- 715 | --[[ This section maps midi events to actions using the following format 716 | { { status, data1, data2 }, { function, optional_args } } 717 | eg. { { 0x90, 0x72, 127 }, { keypress, XK_a } } 718 | eg. { named_event, { myFunction } } 719 | eg. { { 0x90, 0x72, -1 }, { myFunction } } 720 | 721 | if no function arguments are specified then the midi event will be passed to 722 | the function. 723 | the comparison function for midi events works such that a -1 will compare as 724 | equal. 725 | 726 | pre-defined functions are: 727 | * keypress( XK_keycode ) 728 | * keydown( XK_keycode ) 729 | * keyup( XK_keycode ) 730 | * buttonpress( n ) 731 | * buttondown( n ) 732 | * buttonup( n ) 733 | * mousemove( x, y ) 734 | * mousepos( x, y ) 735 | * send_midi( { status, data1, data2 } ) 736 | * exec( "command" ) 737 | ]]-- 738 | 739 | --[[ dbus commands ]]-- 740 | MediaPlayer2 = 'rhythmbox' 741 | dbus_MediaPlayer2 = 'dbus-send --session --type="method_call"' 742 | .. ' --dest=org.mpris.MediaPlayer2.' .. MediaPlayer2 743 | .. ' /org/mpris/MediaPlayer2' 744 | .. ' org.mpris.MediaPlayer2.Player.' -- append command to end 745 | 746 | function dbus_MediaPlayer2_seek( midi ) 747 | exec( dbus_MediaPlayer2 .. 'Seek int64:' .. (midi[3] - 64) * 1000000) 748 | end 749 | 750 | --[[ Default action map ]]-- 751 | default = { 752 | name = "Default Configuration", 753 | map = { 754 | --dbus playPause,next,prev using MediaPlayer2.Player interface 755 | { ddj.deckA.play.down, { exec, dbus_MediaPlayer2 .. 'PlayPause' } }, 756 | { ddj.deckA.autoloop.next, { exec, dbus_MediaPlayer2 .. 'Next' } }, 757 | { ddj.deckA.autoloop.prev, { exec, dbus_MediaPlayer2 .. 'Previous' } }, 758 | { ddj.deckA.jogdial.turn, { dbus_MediaPlayer2_seek } }, 759 | 760 | { ddj.misc.deckC.down, { lbutton_toggle, ddj.misc.deckC } }, 761 | { ddj.misc.deckD.down, { lbutton_toggle, ddj.misc.deckD } }, 762 | { ddj.misc.browse.sup, { midi_send, {0xFF, 0, 0} } }, 763 | { ddj.deckA.sync.down, { lbutton_toggle, ddj.deckA.sync } }, 764 | 765 | { ddj.deckA.ctrlA.down, { lbutton_toggle, ddj.deckA.ctrlA } }, 766 | { ddj.deckA.FX1.down, { lbutton_toggle, ddj.deckA.FX1 } }, 767 | { ddj.deckA.FX2.down, { lbutton_toggle, ddj.deckA.FX2 } }, 768 | { ddj.deckA.FX3.down, { lbutton_toggle, ddj.deckA.FX3 } }, 769 | { ddj.deckA.ctrlB.down, { lbutton_toggle, ddj.deckA.ctrlB } }, 770 | 771 | { ddj.deckB.ctrlA.down, { lbutton_toggle, ddj.deckB.ctrlA } }, 772 | { ddj.deckB.FX1.down, { lbutton_toggle, ddj.deckB.FX1 } }, 773 | { ddj.deckB.FX2.down, { lbutton_toggle, ddj.deckB.FX2 } }, 774 | { ddj.deckB.FX3.down, { lbutton_toggle, ddj.deckB.FX3 } }, 775 | { ddj.deckB.ctrlB.down, { lbutton_toggle, ddj.deckB.ctrlB } }, 776 | 777 | { ddj.deckC.ctrlA.down, { lbutton_toggle, ddj.deckC.ctrlA } }, 778 | { ddj.deckC.FX1.down, { lbutton_toggle, ddj.deckC.FX1 } }, 779 | { ddj.deckC.FX2.down, { lbutton_toggle, ddj.deckC.FX2 } }, 780 | { ddj.deckC.FX3.down, { lbutton_toggle, ddj.deckC.FX3 } }, 781 | { ddj.deckC.ctrlB.down, { lbutton_toggle, ddj.deckC.ctrlB } }, 782 | 783 | { ddj.deckD.ctrlA.down, { lbutton_toggle, ddj.deckD.ctrlA } }, 784 | { ddj.deckD.FX1.down, { lbutton_toggle, ddj.deckD.FX1 } }, 785 | { ddj.deckD.FX2.down, { lbutton_toggle, ddj.deckD.FX2 } }, 786 | { ddj.deckD.FX3.down, { lbutton_toggle, ddj.deckD.FX3 } }, 787 | { ddj.deckD.ctrlB.down, { lbutton_toggle, ddj.deckD.ctrlB } }, 788 | } 789 | } 790 | 791 | -- Lightworks 792 | ------------- 793 | 794 | -- Function to allow using the right jogdial pad in lightworks. 795 | -- needs for lightworks to change keyboard shortcuts for XK_Right & Left 796 | -- to jog right and left. 797 | function lw_jogdial_turn( event ) 798 | if event[3] > 64 then 799 | keypress( XK_Right ) 800 | end 801 | if event[3] < 64 then 802 | keypress( XK_Left ) 803 | end 804 | end 805 | 806 | function lw_movegrid_turn( event ) 807 | if event[3] > 64 then 808 | keypress( XK_a ) 809 | end 810 | if event[3] < 64 then 811 | keypress( XK_s ) 812 | end 813 | end 814 | 815 | -- Action Map 816 | lightworks = { 817 | name = "Lightworks", 818 | map = { 819 | { ddj.deckB.play.down, { keypress, XK_space } }, 820 | { ddj.deckB.jogdial.turn, { lw_jogdial_turn } }, 821 | { ddj.deckB.cue.down, { keypress, XK_a } }, 822 | { ddj.deckB.sampler1.down, { keypress, XK_apostrophe } }, 823 | { ddj.deckB.sampler2.down, { keypress, XK_p } }, 824 | { ddj.deckB.sampler3.down, { keypress, XK_i } }, 825 | { ddj.deckB.sampler4.down, { keypress, XK_o } }, 826 | --{ ddj.deckB.ctrla.down, { keypress, XK_x } }, 827 | { ddj.deckB.autoloop.turn, { lw_movegrid_turn } }, 828 | } 829 | } 830 | 831 | 832 | -- Banshee 833 | ---------- 834 | function banshee_autoloop_turn( event ) 835 | if event[3] < 30 then keypress( XK_n ) end 836 | if event[3] > 100 then keypress( XK_b ) end 837 | end 838 | 839 | Banshee = { 840 | name = "Banshee Media Player", 841 | map = { 842 | { ddj.deckA.play.down, { keypress, XK_space } }, 843 | { ddj.deckA.autoloop.turn, { banshee_autoloop_turn } }, 844 | } 845 | } 846 | 847 | 848 | -- Mixxx 849 | -------- 850 | mixxx = { 851 | name = "Mixxx DJ", 852 | map = nil, 853 | } 854 | 855 | -- VLC 856 | ------ 857 | vlc = { 858 | name = "VideoLAN Client", 859 | map = { 860 | { ddj.deckB.play.down, { keypress, XK_space } }, 861 | } 862 | } 863 | 864 | -- Applications Table 865 | --------------------- 866 | applications = { 867 | ["Banshee"] = Banshee, 868 | ["vlc"] = vlc, 869 | ["mixxx"] = mixxx, 870 | ["ntcardvt"] = lightworks, 871 | } 872 | 873 | function table_rows( t ) 874 | local i = 0 875 | local n = #t 876 | return function () 877 | i = i + 1 878 | if i <= n then return t[i] end 879 | end 880 | end 881 | 882 | --[[ Pattern matcher for messages ]]-- 883 | -- 884 | -- Both pattern and message share the same structure: {status, data1, data2}. 885 | -- For any element of the pattern is equal -1, corresponding element of the 886 | -- message is ignored / considered equal. Third element is often used for 887 | -- continuous measurements such as acceleration, thus in addition to being -1, 888 | -- it can also be nil, i.e. omitted entirely, making it for a table of two 889 | -- elements, like this: {0xb0, 0x15} instead of this: {0xb0, 0x15, -1}. 890 | function message_matches( pattern, message ) 891 | if pattern[1] ~= -1 then 892 | if pattern[1] ~= message[1] then return false end 893 | end 894 | if pattern[2] ~= -1 then 895 | if pattern[2] ~= message[2] then return false end 896 | end 897 | if pattern[3] ~= nil and pattern[3] ~= -1 then 898 | if pattern[3] ~= message[3] then return false end 899 | end 900 | return true 901 | end 902 | 903 | --[[ Input Event Handler ]]-- 904 | function midi_recv( status, data1, data2 ) 905 | local message = { status, data1, data2 } 906 | 907 | -- select application based on wm_name pulled from X11 908 | local app = applications[ wm_class ] 909 | if( not app ) then 910 | app = default 911 | end 912 | local table = app.map 913 | 914 | -- look for control in the application map first, before the default map 915 | while( table ) do 916 | -- search items 917 | for row in table_rows( table ) do 918 | -- test for event 919 | if( message_matches( row[ 1 ], message ) ) then 920 | local fn = row[ 2 ][ 1 ] 921 | local arg = row[ 2 ][ 2 ] 922 | -- check for argument in second pair 923 | if( arg == nil ) then 924 | arg = message 925 | end 926 | fn( arg ) 927 | return 928 | end 929 | end 930 | --if not found in application.map search default 931 | if( table == default.map ) then table = nil 932 | else table = default.map 933 | end 934 | end 935 | end 936 | --------------------------------------------------------------------------------