midi-shell-commands 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +128 -0
  2. package/index.js +67 -0
  3. package/package.json +19 -0
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # MIDI Shell Commands
2
+
3
+ Execute shell scripts when MIDI events occur.
4
+
5
+ This tool listens to all available MIDI inputs on your system and, when a MIDI note event is received, it looks for an executable file in
6
+ your scripts directory whose name matches the event. If a match is found, it runs that script.
7
+
8
+ ## How it works
9
+
10
+ - On startup, the app scans the directory you provide for scripts (non-hidden files) and remembers the names without their extensions.
11
+ - It listens to MIDI inputs using the `easymidi` library.
12
+ - For each incoming note event (`noteon` or `noteoff`), it tries these filename patterns, from most specific to least specific:
13
+ - `{_type}.{note}.{channel}.{velocity}`
14
+ - `{_type}.{note}.{channel}`
15
+ - `{_type}.{note}`
16
+
17
+ Where:
18
+
19
+ - `_type` is `noteon` or `noteoff`.
20
+ - `note` is a MIDI note number (0–127).
21
+ - `channel` is the MIDI channel (0–15).
22
+ - `velocity` is the event velocity (0–127).
23
+
24
+ If the base name of a script (filename without its extension) exactly matches one of those patterns, that script will be executed.
25
+
26
+ Examples based on the included sample scripts:
27
+
28
+ - `noteon.72.sh` will execute on any Note On for note 72, any channel, any velocity.
29
+ - `noteon.72.0.127.sh` will execute only for Note On of note 72 on channel 0 with velocity 127.
30
+
31
+ The application periodically checks for new MIDI input devices and will start listening to them automatically.
32
+
33
+ ## Requirements
34
+
35
+ - Node.js 14+ (any modern LTS should work)
36
+ - A system with MIDI inputs accessible to Node (virtual or physical)
37
+
38
+ ## Installation
39
+
40
+ 1. Clone this repository.
41
+ 2. Install dependencies:
42
+
43
+ npm install
44
+
45
+ 3. Ensure your scripts are executable (Unix/macOS):
46
+
47
+ chmod +x ./scripts/*.sh
48
+
49
+ ## Usage
50
+
51
+ You can run the tool in a few ways:
52
+
53
+ - After a global install (recommended):
54
+
55
+ midi-shell-commands ./scripts
56
+
57
+ - Using npx without installing globally:
58
+
59
+ npx midi-shell-commands ./scripts
60
+
61
+ - Using the provided npm script (watches the `./scripts` directory):
62
+
63
+ npm start
64
+
65
+ - Or directly with Node, specifying the path to your scripts directory:
66
+
67
+ node index.js ./scripts
68
+
69
+ Replace `./scripts` with the path to your own directory containing executable scripts.
70
+
71
+ On launch, you should see console output when a matching script is executed, for example:
72
+
73
+ Executing noteon.72.sh
74
+
75
+ ## Writing scripts
76
+
77
+ - Place your executable files in the directory you pass to the app.
78
+ - Name your scripts according to one of the supported patterns. The extension can be anything; the match is done on the filename without the
79
+ extension.
80
+ - Scripts are executed with the working directory set to the scripts directory, so relative paths within your script are relative to that
81
+ directory.
82
+ - You can write scripts in any language available on your system (sh, bash, Python, etc.). Ensure the shebang is present and the file is
83
+ executable.
84
+
85
+ Example Bash script (saved as `scripts/noteon.60.sh`):
86
+
87
+ ```
88
+ #!/usr/bin/env bash
89
+ echo "Middle C was pressed!" >> ./midi-log.txt
90
+ ```
91
+
92
+ Make it executable:
93
+
94
+ ```
95
+ chmod +x scripts/noteon.60.sh
96
+ ```
97
+
98
+ ## Notes and behavior
99
+
100
+ - Only non-hidden files in the scripts directory are considered (files not starting with a dot).
101
+ - Matching is exact and case-sensitive on the base filename.
102
+ - Multiple scripts can exist; if multiple basenames match different events, each will run when its event occurs.
103
+ - If multiple files share the same basename with different extensions (e.g., `.sh`, `.py`), each event will run the one that matches by
104
+ basename encountered in the initial directory scan order. Multiple scripts can get invoked by a single note.
105
+ - The app listens for `noteon` and `noteoff`. Other MIDI events are currently ignored.
106
+ - The process will close all MIDI inputs on exit.
107
+
108
+ ## Troubleshooting
109
+
110
+ - "Please pass the path to the directory containing your scripts." — You must supply the scripts directory path as the last CLI argument (or
111
+ use `npm start`).
112
+ - No scripts are executing:
113
+ - Verify your file is executable (`chmod +x your-script`).
114
+ - Double-check the filename exactly matches one of the patterns for the event you expect.
115
+ - Confirm your MIDI device is connected and appears in your system. The app uses `easymidi.getInputs()` to enumerate inputs.
116
+ - Add simple logging (e.g., `echo`) inside your script to verify execution.
117
+ - Permission denied when executing scripts: ensure the script has the executable bit set and that your user has permission to run it.
118
+
119
+ ## Development
120
+
121
+ - Main entry: `index.js`
122
+ - Dependencies: `easymidi`
123
+ - Helpful npm scripts:
124
+ - `npm start` — starts the watcher using `./scripts` as the scripts directory.
125
+
126
+ ## License
127
+
128
+ ISC
package/index.js ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ const childProcess = require('child_process');
3
+ const easyMIDI = require('easymidi');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ if (process.argv.length === 2) {
8
+ console.error('Please pass the path to the directory containing your scripts.');
9
+ process.exit(1);
10
+ }
11
+
12
+ const watchDir = path.resolve(process.argv[process.argv.length - 1]);
13
+ const scripts = fs.readdirSync(watchDir).filter(f => f[0] !== '.');
14
+ const scriptsWithoutExtension = scripts.map(script => script.split('.').slice(0, -1).join('.'));
15
+ const checkDelay = 60 * 1000 + (Math.random() * 3000) | 0;
16
+
17
+ const watchedInputs = {};
18
+ checkInputs();
19
+ setInterval(checkInputs, checkDelay);
20
+
21
+ function checkInputs() {
22
+ const inputNames = easyMIDI.getInputs();
23
+ for (const inputName of inputNames) {
24
+ listenToInput(inputName);
25
+ }
26
+ }
27
+
28
+ function listenToInput(inputName) {
29
+ if (watchedInputs[inputName]) {
30
+ return;
31
+ }
32
+ const input = watchedInputs[inputName] = new easyMIDI.Input(inputName);
33
+ input.on('noteon', invokeScripts);
34
+ input.on('noteoff', invokeScripts);
35
+ }
36
+
37
+ function invokeScripts(msg) {
38
+ const possibleFileNames = mapMessageToFileNames(msg);
39
+ for (const possibleFileName of possibleFileNames) {
40
+ for (let i = 0; i < scriptsWithoutExtension.length; i++) {
41
+ if (scriptsWithoutExtension[i] === possibleFileName) {
42
+ console.log('Executing ' + scripts[i]);
43
+ childProcess.exec(`./${scripts[i]}`, { cwd: watchDir }, reportErrors);
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ function mapMessageToFileNames(msg) {
50
+ return [
51
+ `${msg._type}.${msg.note}.${msg.channel}.${msg.velocity}`,
52
+ `${msg._type}.${msg.note}.${msg.channel}`,
53
+ `${msg._type}.${msg.note}`,
54
+ ];
55
+ }
56
+
57
+ function reportErrors(err) {
58
+ if (err) {
59
+ console.error(err);
60
+ }
61
+ }
62
+
63
+ process.on('exit', () => {
64
+ for (const input of Object.values(watchedInputs)) {
65
+ input.close();
66
+ }
67
+ });
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "midi-shell-commands",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "bin": {
6
+ "midi-shell-commands": "index.js"
7
+ },
8
+ "scripts": {
9
+ "start": "node index.js ./scripts",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [],
13
+ "author": "",
14
+ "license": "ISC",
15
+ "description": "",
16
+ "dependencies": {
17
+ "easymidi": "^3.1.0"
18
+ }
19
+ }