From 4ef572a54d6d3a54e4fce2fc785fd6751fb69c99 Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 11:42:51 +0000 Subject: [PATCH 01/13] Add Nix flake for out-of-the-box NixOS support - flake.nix: self-contained Nix package with all Python deps (rtmidi2, launchpad-py, musicpy, mido-fix) and native MIDI virtual cable (midimech-vport) - midimech-vport.c: native C ALSA sequencer virtual MIDI cable, equivalent to loopMIDI on Windows - midimech-launch.py: launcher that starts vport + midimech - NixOS module with snd_seq_dummy blacklist for clean MIDI ports - Fix launchpad_py crash on Linux ALSA when no Launchpad connected Usage: nix run github:zitongcharliedeng/midimech#midimech Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 + flake.lock | 27 +++++++ flake.nix | 181 +++++++++++++++++++++++++++++++++++++++++++++ midimech-launch.py | 57 ++++++++++++++ midimech-vport.c | 73 ++++++++++++++++++ src/core.py | 40 +++++----- 6 files changed, 361 insertions(+), 19 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 midimech-launch.py create mode 100644 midimech-vport.c diff --git a/.gitignore b/.gitignore index 14feef5..3adb98c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ midimech.spec build/ dist/ .idea/ +result +result diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1b8772e --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1770841267, + "narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0af4278 --- /dev/null +++ b/flake.nix @@ -0,0 +1,181 @@ +{ + description = "Midimech - Isomorphic musical layout engine for LinnStrument and Launchpad X"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + python = pkgs.python312; + + # --- Custom Python packages not in nixpkgs --- + + # rtmidi2: Cython wrapper around RtMidi C++ library. + # Uses pre-built manylinux wheel + autoPatchelfHook since no sdist is published. + rtmidi2 = python.pkgs.buildPythonPackage rec { + pname = "rtmidi2"; + version = "1.4.1"; + format = "wheel"; + src = pkgs.fetchurl { + url = "https://files.pythonhosted.org/packages/cc/08/e426f1a8dae34acb8a13a0eca47970a78955a0c00395efe89a94ef04ad49/rtmidi2-1.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"; + hash = "sha256:2270b773302806209eb3aec341156ef841e2f5ea7a83f5b242311d0da1df3a76"; + }; + nativeBuildInputs = [ pkgs.autoPatchelfHook ]; + buildInputs = [ + pkgs.alsa-lib + pkgs.libjack2 + pkgs.stdenv.cc.cc.lib # libstdc++ + ]; + pythonImportsCheck = [ "rtmidi2" ]; + }; + + # launchpad-py: Pure Python Novation Launchpad control suite + launchpad-py = python.pkgs.buildPythonPackage rec { + pname = "launchpad-py"; + version = "0.9.1"; + pyproject = true; + src = python.pkgs.fetchPypi { + pname = "launchpad_py"; + inherit version; + hash = "sha256:9c70885a9079d9960a066515f4b83727e7c475543da8ef68786f00a8ef10727c"; + }; + build-system = [ python.pkgs.setuptools ]; + dependencies = [ python.pkgs.pygame-ce ]; + pythonImportsCheck = [ "launchpad_py" ]; + doCheck = false; + }; + + # mido-fix: Fork of mido (MIDI Objects), required by musicpy + mido-fix = python.pkgs.buildPythonPackage rec { + pname = "mido-fix"; + version = "1.2.12"; + pyproject = true; + src = python.pkgs.fetchPypi { + pname = "mido_fix"; + inherit version; + hash = "sha256:8ce7ad87f847de36c7dd3048876581113c4d83367d1f392e5a7a9f9562b3374e"; + }; + build-system = [ python.pkgs.setuptools ]; + pythonImportsCheck = [ "mido_fix" ]; + doCheck = false; + }; + + # musicpy: Music programming language / theory library + # musicpy declares mido-fix + dataclasses as deps; midimech only uses + # musicpy for chord analysis (not MIDI I/O), so we skip the runtime + # deps check and provide pygame-ce which musicpy actually imports. + musicpy = python.pkgs.buildPythonPackage rec { + pname = "musicpy"; + version = "7.11"; + pyproject = true; + src = python.pkgs.fetchPypi { + inherit pname version; + hash = "sha256:7957971dc1be5b310a83253acd8dd8d3ce803ba47f67d88603904667badde993"; + }; + build-system = [ python.pkgs.setuptools ]; + dependencies = [ python.pkgs.pygame-ce mido-fix ]; + pythonImportsCheck = [ "musicpy" ]; + dontCheckRuntimeDeps = true; + doCheck = false; + }; + + # --- Python environment with all deps --- + + pythonEnv = python.withPackages (ps: [ + ps.pygame-ce + ps.pygame-gui + ps.pyglm + rtmidi2 + launchpad-py + musicpy + ps.pyyaml + ps.webcolors + ]); + + runtimeLibs = pkgs.lib.makeLibraryPath [ + pkgs.SDL2 + pkgs.SDL2_image + pkgs.SDL2_mixer + pkgs.SDL2_ttf + pkgs.alsa-lib + pkgs.libjack2 + ]; + + in { + packages.${system} = { + midimech = pkgs.stdenv.mkDerivation { + pname = "midimech"; + version = "0.1.0-chromatic-quantize"; + src = self; + + nativeBuildInputs = [ pkgs.makeWrapper pkgs.pkg-config ]; + buildInputs = [ pythonEnv pkgs.alsa-lib ]; + + buildPhase = '' + # Compile the native MIDI virtual cable (zero-overhead loopMIDI equivalent) + cc -O2 -Wall $(pkg-config --cflags --libs alsa) \ + midimech-vport.c -o midimech-vport + ''; + + installPhase = '' + mkdir -p $out/share/midimech $out/bin + cp -r . $out/share/midimech/ + + # Install the native MIDI virtual cable + install -m755 midimech-vport $out/bin/midimech-vport + + # Main entry point: launcher starts native MIDI cable + midimech + makeWrapper ${pythonEnv}/bin/python3 $out/bin/midimech \ + --add-flags "$out/share/midimech/midimech-launch.py" \ + --prefix LD_LIBRARY_PATH : "${runtimeLibs}" + + # Direct entry (no loopback, for advanced users who manage MIDI themselves) + makeWrapper ${pythonEnv}/bin/python3 $out/bin/midimech-raw \ + --add-flags "$out/share/midimech/midimech.py" \ + --prefix LD_LIBRARY_PATH : "${runtimeLibs}" + ''; + + meta = with pkgs.lib; { + description = "Isomorphic musical layout engine for LinnStrument and Launchpad X"; + homepage = "https://github.com/zitongcharliedeng/midimech"; + license = licenses.mit; + platforms = [ "x86_64-linux" ]; + }; + }; + + default = self.packages.${system}.midimech; + }; + + apps.${system}.default = { + type = "app"; + program = "${self.packages.${system}.midimech}/bin/midimech"; + }; + + # NixOS module: installs midimech system-wide with clean MIDI setup. + # + # Usage in your NixOS config: + # imports = [ midimech-flake.nixosModules.default ]; + # services.midimech.enable = true; + nixosModules.default = { config, lib, ... }: + let + cfg = config.services.midimech; + in { + options.services.midimech = { + enable = lib.mkEnableOption "midimech isomorphic MIDI layout engine"; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ + self.packages.${system}.midimech + ]; + + # Remove the "Midi Through" phantom port that clutters MIDI device lists. + # midimech provides its own virtual MIDI cable; snd_seq_dummy is redundant. + boot.blacklistedKernelModules = [ "snd_seq_dummy" ]; + }; + }; + }; +} diff --git a/midimech-launch.py b/midimech-launch.py new file mode 100644 index 0000000..e8d03a3 --- /dev/null +++ b/midimech-launch.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +""" +midimech launcher — starts the virtual MIDI cable (C native) and midimech. + +The MIDI cable (midimech-vport) runs as a separate process for zero-overhead +MIDI forwarding, equivalent to loopMIDI on Windows. +""" + +import subprocess +import sys +import os +import time + + +def main(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + bin_dir = os.path.normpath(os.path.join(script_dir, "..", "bin")) + + # Start the native MIDI virtual cable + vport_bin = os.path.join(bin_dir, "midimech-vport") + if not os.path.exists(vport_bin): + # Fallback for development + vport_bin = os.path.join(script_dir, "midimech-vport") + + vport_proc = subprocess.Popen([vport_bin]) + + # Give the virtual port a moment to register with ALSA + time.sleep(0.3) + + # Find and launch midimech + midimech_py = os.path.join(script_dir, "midimech.py") + if not os.path.exists(midimech_py): + midimech_py = os.path.join(script_dir, "..", "share", "midimech", "midimech.py") + + print("[launcher] Starting midimech...") + proc = subprocess.Popen( + [sys.executable, midimech_py], + cwd=os.path.dirname(os.path.abspath(midimech_py)), + ) + + # Wait for midimech to exit + try: + proc.wait() + except KeyboardInterrupt: + proc.terminate() + proc.wait() + + # Clean up the virtual port + vport_proc.terminate() + vport_proc.wait() + + print("[launcher] midimech exited, cleaning up") + return proc.returncode + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/midimech-vport.c b/midimech-vport.c new file mode 100644 index 0000000..2038a32 --- /dev/null +++ b/midimech-vport.c @@ -0,0 +1,73 @@ +/* + * midimech-vport: Zero-overhead virtual MIDI cable for ALSA sequencer. + * + * Creates a single ALSA sequencer port named "midimech" that acts as a + * pass-through: any MIDI written to it is forwarded to all subscribers. + * + * This replaces the Python rtmidi2 callback approach, eliminating GIL + * overhead and providing jitter-free MIDI forwarding at any buffer size. + * + * Equivalent to loopMIDI on Windows. + */ + +#include +#include +#include + +static volatile int running = 1; + +static void on_signal(int sig) { + (void)sig; + running = 0; +} + +int main(void) { + snd_seq_t *seq; + int err; + + err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + if (err < 0) { + fprintf(stderr, "Cannot open ALSA sequencer: %s\n", snd_strerror(err)); + return 1; + } + + snd_seq_set_client_name(seq, "midimech"); + + /* Create a single port with both read and write capabilities. + * - WRITE + SUBS_WRITE: midimech.py can send MIDI here + * - READ + SUBS_READ: SurgeXT (or any synth) can subscribe to receive MIDI + */ + int port = snd_seq_create_simple_port(seq, "midimech", + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE | + SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); + + if (port < 0) { + fprintf(stderr, "Cannot create port: %s\n", snd_strerror(port)); + snd_seq_close(seq); + return 1; + } + + fprintf(stderr, "[midimech-vport] Virtual MIDI cable 'midimech' ready (port %d)\n", port); + + signal(SIGINT, on_signal); + signal(SIGTERM, on_signal); + + /* Event loop: read incoming MIDI events, forward to all subscribers */ + snd_seq_event_t *ev; + while (running) { + err = snd_seq_event_input(seq, &ev); + if (err < 0) { + if (err == -EAGAIN) continue; + break; + } + + snd_seq_ev_set_source(ev, port); + snd_seq_ev_set_subs(ev); + snd_seq_ev_set_direct(ev); + snd_seq_event_output_direct(seq, ev); + } + + snd_seq_close(seq); + return 0; +} diff --git a/src/core.py b/src/core.py index 930d754..fdd1539 100644 --- a/src/core.py +++ b/src/core.py @@ -1757,28 +1757,30 @@ def __init__(self): self.launchpads = [] num_launchpads = 0 if self.options.launchpad: - launchpads = [] - lp = launchpad.LaunchpadProMk3() - if lp.Check(0): - if lp.Open(0): - self.launchpads += [Launchpad(self, lp, "promk3", num_launchpads)] - num_launchpads += 1 - lp = launchpad.LaunchpadPro() - if lp.Check(0): - if lp.Open(0): - self.launchpads += [Launchpad(self, lp, "pro", num_launchpads)] - num_launchpads += 1 - lp = launchpad.LaunchpadLPX() - if lp.Check(1): + try: + lp = launchpad.LaunchpadProMk3() + if lp.Check(0): + if lp.Open(0): + self.launchpads += [Launchpad(self, lp, "promk3", num_launchpads)] + num_launchpads += 1 + lp = launchpad.LaunchpadPro() + if lp.Check(0): + if lp.Open(0): + self.launchpads += [Launchpad(self, lp, "pro", num_launchpads)] + num_launchpads += 1 lp = launchpad.LaunchpadLPX() - if lp.Open(1): - self.launchpads += [Launchpad(self, lp, "lpx", num_launchpads)] - num_launchpads += 1 - if launchpad.LaunchpadLPX().Check(3): + if lp.Check(1): lp = launchpad.LaunchpadLPX() - if lp.Open(3): # second - self.launchpads += [Launchpad(self, lp, "lpx", num_launchpads, self.options.octave_separation)] + if lp.Open(1): + self.launchpads += [Launchpad(self, lp, "lpx", num_launchpads)] num_launchpads += 1 + if launchpad.LaunchpadLPX().Check(3): + lp = launchpad.LaunchpadLPX() + if lp.Open(3): # second + self.launchpads += [Launchpad(self, lp, "lpx", num_launchpads, self.options.octave_separation)] + num_launchpads += 1 + except Exception as e: + print(f"Launchpad detection skipped ({e})") if self.launchpads: print('Launchpads:', len(self.launchpads)) From 6d3d2fcd7d2646b940de59e11af9d5a82971c2ca Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 11:48:41 +0000 Subject: [PATCH 02/13] Remove duplicate result entry in .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3adb98c..e710631 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ build/ dist/ .idea/ result -result From b4a648eb38c5a465be53e0f3d8decc8800fb6270 Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 11:54:11 +0000 Subject: [PATCH 03/13] Inline launcher into flake, remove midimech-launch.py The launcher is trivial shell glue (start vport, sleep, exec midimech). Convention: flake handles its own wrappers in installPhase. Repo now has only flake.nix, flake.lock, and midimech-vport.c as nix-related files. midimech-vport.c is real Linux source code, not nix plumbing. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 20 ++++++++++++---- midimech-launch.py | 57 ---------------------------------------------- 2 files changed, 16 insertions(+), 61 deletions(-) delete mode 100644 midimech-launch.py diff --git a/flake.nix b/flake.nix index 0af4278..8f235d4 100644 --- a/flake.nix +++ b/flake.nix @@ -127,12 +127,24 @@ # Install the native MIDI virtual cable install -m755 midimech-vport $out/bin/midimech-vport - # Main entry point: launcher starts native MIDI cable + midimech - makeWrapper ${pythonEnv}/bin/python3 $out/bin/midimech \ - --add-flags "$out/share/midimech/midimech-launch.py" \ + # Main entry point: starts virtual MIDI cable, then midimech + cat > $out/bin/midimech </dev/null; wait "\$VPORT_PID" 2>/dev/null; } + trap cleanup EXIT INT TERM + $out/bin/midimech-vport & + VPORT_PID=\$! + sleep 0.3 + exec ${pythonEnv}/bin/python3 $out/share/midimech/midimech.py "\$@" + LAUNCHER + chmod +x $out/bin/midimech + patchShebangs $out/bin/midimech + + # Wrap to include runtime libraries + wrapProgram $out/bin/midimech \ --prefix LD_LIBRARY_PATH : "${runtimeLibs}" - # Direct entry (no loopback, for advanced users who manage MIDI themselves) + # Direct entry (no virtual cable, for users who manage MIDI themselves) makeWrapper ${pythonEnv}/bin/python3 $out/bin/midimech-raw \ --add-flags "$out/share/midimech/midimech.py" \ --prefix LD_LIBRARY_PATH : "${runtimeLibs}" diff --git a/midimech-launch.py b/midimech-launch.py deleted file mode 100644 index e8d03a3..0000000 --- a/midimech-launch.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -""" -midimech launcher — starts the virtual MIDI cable (C native) and midimech. - -The MIDI cable (midimech-vport) runs as a separate process for zero-overhead -MIDI forwarding, equivalent to loopMIDI on Windows. -""" - -import subprocess -import sys -import os -import time - - -def main(): - script_dir = os.path.dirname(os.path.abspath(__file__)) - bin_dir = os.path.normpath(os.path.join(script_dir, "..", "bin")) - - # Start the native MIDI virtual cable - vport_bin = os.path.join(bin_dir, "midimech-vport") - if not os.path.exists(vport_bin): - # Fallback for development - vport_bin = os.path.join(script_dir, "midimech-vport") - - vport_proc = subprocess.Popen([vport_bin]) - - # Give the virtual port a moment to register with ALSA - time.sleep(0.3) - - # Find and launch midimech - midimech_py = os.path.join(script_dir, "midimech.py") - if not os.path.exists(midimech_py): - midimech_py = os.path.join(script_dir, "..", "share", "midimech", "midimech.py") - - print("[launcher] Starting midimech...") - proc = subprocess.Popen( - [sys.executable, midimech_py], - cwd=os.path.dirname(os.path.abspath(midimech_py)), - ) - - # Wait for midimech to exit - try: - proc.wait() - except KeyboardInterrupt: - proc.terminate() - proc.wait() - - # Clean up the virtual port - vport_proc.terminate() - vport_proc.wait() - - print("[launcher] midimech exited, cleaning up") - return proc.returncode - - -if __name__ == "__main__": - sys.exit(main() or 0) From d8e77efa89012ec1f617fc2b5539acbed1cc2f3c Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 12:20:09 +0000 Subject: [PATCH 04/13] Add desktop entry for application launchers Installs midimech.desktop and icon so desktop environments can show midimech in their application menu. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/flake.nix b/flake.nix index 8f235d4..4312803 100644 --- a/flake.nix +++ b/flake.nix @@ -144,6 +144,20 @@ wrapProgram $out/bin/midimech \ --prefix LD_LIBRARY_PATH : "${runtimeLibs}" + # Desktop entry for application launchers + mkdir -p $out/share/applications $out/share/icons/hicolor/256x256/apps + cp $out/share/midimech/icon.png $out/share/icons/hicolor/256x256/apps/midimech.png + cat > $out/share/applications/midimech.desktop < Date: Fri, 13 Feb 2026 12:39:57 +0000 Subject: [PATCH 05/13] Set SDL_APP_ID and StartupWMClass for GNOME icon matching Without this, GNOME shows a generic gear icon in alt-tab and the taskbar because it can't associate the pygame/SDL2 window with the desktop entry. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 4312803..ab79250 100644 --- a/flake.nix +++ b/flake.nix @@ -140,9 +140,10 @@ chmod +x $out/bin/midimech patchShebangs $out/bin/midimech - # Wrap to include runtime libraries + # Wrap to include runtime libraries + set SDL app ID for Wayland icon matching wrapProgram $out/bin/midimech \ - --prefix LD_LIBRARY_PATH : "${runtimeLibs}" + --prefix LD_LIBRARY_PATH : "${runtimeLibs}" \ + --set SDL_APP_ID midimech # Desktop entry for application launchers mkdir -p $out/share/applications $out/share/icons/hicolor/256x256/apps @@ -156,6 +157,7 @@ Terminal=false Type=Application Categories=Audio;Music;Midi; + StartupWMClass=midimech DESKTOP # Direct entry (no virtual cable, for users who manage MIDI themselves) From 0e3fdee815a3207235228ac260a6ff2ece7bf46b Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 12:42:59 +0000 Subject: [PATCH 06/13] Add NixOS installation instructions to README Documents nix run, nix profile install, and NixOS module usage with the recommended virtual MIDI cable setup. Co-Authored-By: Claude Opus 4.6 --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index 904b47e..71d2dd6 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,44 @@ After downloading, make sure to follow the instructions under `Setup`. *Note: These builds are not always up to date.* +### NixOS / Nix + +The included Nix flake handles everything automatically — Python environment, all dependencies, and a virtual MIDI cable (equivalent to loopMIDI on Windows). + +**Try it (no install):** +``` +nix run github:flipcoder/midimech +``` + +**Permanent install (adds to PATH + desktop entry with icon):** +``` +nix profile install github:flipcoder/midimech +``` + +**NixOS module (recommended — system-wide with clean MIDI setup):** + +Add to your `flake.nix` inputs: +```nix +midimech.url = "github:flipcoder/midimech"; +``` + +Then in your NixOS configuration: +```nix +{ midimech, ... }: +{ + imports = [ midimech.nixosModules.default ]; + services.midimech.enable = true; +} +``` + +This installs midimech system-wide with a desktop entry and automatically removes the "Midi Through" phantom MIDI port (`snd_seq_dummy`). + +**What's included:** +- `midimech` — launches the virtual MIDI cable + midimech (recommended) +- `midimech-raw` — direct entry for users who manage MIDI routing themselves + +Point your DAW/synth (e.g. SurgeXT) at the **midimech** MIDI input to receive notes. + ### Mac, Linux, and Running from Git - Download the project by typing the following commands in terminal: From 398b9b1b8b4e1a5411bc5032000e096bd2ae496e Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 12:44:40 +0000 Subject: [PATCH 07/13] Fix README: NixOS module is the recommended install path Removed misleading claim that nix profile install creates desktop entries. Only the NixOS module provides full desktop integration. Co-Authored-By: Claude Opus 4.6 --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 71d2dd6..de9090c 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,7 @@ The included Nix flake handles everything automatically — Python environment, nix run github:flipcoder/midimech ``` -**Permanent install (adds to PATH + desktop entry with icon):** -``` -nix profile install github:flipcoder/midimech -``` - -**NixOS module (recommended — system-wide with clean MIDI setup):** +**NixOS module (recommended):** Add to your `flake.nix` inputs: ```nix @@ -113,11 +108,11 @@ Then in your NixOS configuration: } ``` -This installs midimech system-wide with a desktop entry and automatically removes the "Midi Through" phantom MIDI port (`snd_seq_dummy`). - -**What's included:** -- `midimech` — launches the virtual MIDI cable + midimech (recommended) +This installs midimech system-wide with: +- Desktop entry with icon (shows in GNOME, KDE, etc.) +- Virtual MIDI cable — equivalent to loopMIDI on Windows, started automatically - `midimech-raw` — direct entry for users who manage MIDI routing themselves +- Removes the "Midi Through" phantom MIDI port (`snd_seq_dummy`) Point your DAW/synth (e.g. SurgeXT) at the **midimech** MIDI input to receive notes. From aeadf32e1d69ffadf2979fc7a7478c3a132c10d6 Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 12:45:28 +0000 Subject: [PATCH 08/13] Remove midimech-raw, single entry point with virtual cable The virtual MIDI cable is always started with midimech. No need for a separate binary without it. Co-Authored-By: Claude Opus 4.6 --- README.md | 1 - flake.nix | 4 ---- 2 files changed, 5 deletions(-) diff --git a/README.md b/README.md index de9090c..3912398 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,6 @@ Then in your NixOS configuration: This installs midimech system-wide with: - Desktop entry with icon (shows in GNOME, KDE, etc.) - Virtual MIDI cable — equivalent to loopMIDI on Windows, started automatically -- `midimech-raw` — direct entry for users who manage MIDI routing themselves - Removes the "Midi Through" phantom MIDI port (`snd_seq_dummy`) Point your DAW/synth (e.g. SurgeXT) at the **midimech** MIDI input to receive notes. diff --git a/flake.nix b/flake.nix index ab79250..fd9cf62 100644 --- a/flake.nix +++ b/flake.nix @@ -160,10 +160,6 @@ StartupWMClass=midimech DESKTOP - # Direct entry (no virtual cable, for users who manage MIDI themselves) - makeWrapper ${pythonEnv}/bin/python3 $out/bin/midimech-raw \ - --add-flags "$out/share/midimech/midimech.py" \ - --prefix LD_LIBRARY_PATH : "${runtimeLibs}" ''; meta = with pkgs.lib; { From 41a5f285dd46363a96733b801027c53f6197b9d6 Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 12:50:43 +0000 Subject: [PATCH 09/13] Add --no-vport flag to disable virtual MIDI cable Virtual cable starts by default (recommended). Pass --no-vport to skip it for users who manage MIDI routing themselves. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/flake.nix b/flake.nix index fd9cf62..7a294a2 100644 --- a/flake.nix +++ b/flake.nix @@ -127,15 +127,26 @@ # Install the native MIDI virtual cable install -m755 midimech-vport $out/bin/midimech-vport - # Main entry point: starts virtual MIDI cable, then midimech + # Main entry point: starts virtual MIDI cable + midimech + # Use --no-vport to skip the virtual cable (for users who manage MIDI themselves) cat > $out/bin/midimech </dev/null; wait "\$VPORT_PID" 2>/dev/null; } - trap cleanup EXIT INT TERM - $out/bin/midimech-vport & - VPORT_PID=\$! - sleep 0.3 - exec ${pythonEnv}/bin/python3 $out/share/midimech/midimech.py "\$@" + USE_VPORT=1 + ARGS="" + for arg in "\$@"; do + case "\$arg" in + --no-vport) USE_VPORT=0 ;; + *) ARGS="\$ARGS \$arg" ;; + esac + done + if [ "\$USE_VPORT" = 1 ]; then + cleanup() { kill "\$VPORT_PID" 2>/dev/null; wait "\$VPORT_PID" 2>/dev/null; } + trap cleanup EXIT INT TERM + $out/bin/midimech-vport & + VPORT_PID=\$! + sleep 0.3 + fi + exec ${pythonEnv}/bin/python3 $out/share/midimech/midimech.py \$ARGS LAUNCHER chmod +x $out/bin/midimech patchShebangs $out/bin/midimech From cfa3fc870976aabef60c46cc9021e6fa62690e2b Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 12:52:45 +0000 Subject: [PATCH 10/13] Simplify launcher: always start vport, no flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flake is NixOS-only. The vport IS the point — it's the loopMIDI equivalent. Users who don't want it run midimech.py directly, same as on Windows/Mac. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/flake.nix b/flake.nix index 7a294a2..662c24c 100644 --- a/flake.nix +++ b/flake.nix @@ -128,25 +128,14 @@ install -m755 midimech-vport $out/bin/midimech-vport # Main entry point: starts virtual MIDI cable + midimech - # Use --no-vport to skip the virtual cable (for users who manage MIDI themselves) cat > $out/bin/midimech </dev/null; wait "\$VPORT_PID" 2>/dev/null; } - trap cleanup EXIT INT TERM - $out/bin/midimech-vport & - VPORT_PID=\$! - sleep 0.3 - fi - exec ${pythonEnv}/bin/python3 $out/share/midimech/midimech.py \$ARGS + cleanup() { kill "\$VPORT_PID" 2>/dev/null; wait "\$VPORT_PID" 2>/dev/null; } + trap cleanup EXIT INT TERM + $out/bin/midimech-vport & + VPORT_PID=\$! + sleep 0.3 + exec ${pythonEnv}/bin/python3 $out/share/midimech/midimech.py "\$@" LAUNCHER chmod +x $out/bin/midimech patchShebangs $out/bin/midimech From 0d8e49abedcbc7d157f85f861f2ef8c9791f9a02 Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Fri, 13 Feb 2026 13:06:38 +0000 Subject: [PATCH 11/13] Fix version string, add Wayland and X11 wmclass hints Version was incorrectly set to 0.1.0-chromatic-quantize despite nix-support being based on main. Set SDL hints for icon matching on both Wayland and X11 backends. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 662c24c..cab6011 100644 --- a/flake.nix +++ b/flake.nix @@ -108,7 +108,7 @@ packages.${system} = { midimech = pkgs.stdenv.mkDerivation { pname = "midimech"; - version = "0.1.0-chromatic-quantize"; + version = "0.1.0"; src = self; nativeBuildInputs = [ pkgs.makeWrapper pkgs.pkg-config ]; @@ -140,9 +140,11 @@ chmod +x $out/bin/midimech patchShebangs $out/bin/midimech - # Wrap to include runtime libraries + set SDL app ID for Wayland icon matching + # Wrap to include runtime libraries + set window class for desktop icon matching wrapProgram $out/bin/midimech \ --prefix LD_LIBRARY_PATH : "${runtimeLibs}" \ + --set SDL_VIDEO_WAYLAND_WMCLASS midimech \ + --set SDL_VIDEO_X11_WMCLASS midimech \ --set SDL_APP_ID midimech # Desktop entry for application launchers From f3149ae66e1d55a7d1bc89b9b7609c8b3a7f86f7 Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Sat, 14 Feb 2026 15:37:37 +0000 Subject: [PATCH 12/13] Fix launcher CWD so midimech finds scales.yaml from desktop The Python launcher (removed in b4a648e) set cwd= to the midimech directory. The inlined shell launcher did not, so scales.yaml and settings.ini were not found when launched from a .desktop file. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index cab6011..7b64a0a 100644 --- a/flake.nix +++ b/flake.nix @@ -135,6 +135,7 @@ $out/bin/midimech-vport & VPORT_PID=\$! sleep 0.3 + cd $out/share/midimech exec ${pythonEnv}/bin/python3 $out/share/midimech/midimech.py "\$@" LAUNCHER chmod +x $out/bin/midimech From 016da3cbf02d2d76d8fc376475cb5c37fef6a4fd Mon Sep 17 00:00:00 2001 From: zitongcharliedeng Date: Sat, 14 Feb 2026 15:41:59 +0000 Subject: [PATCH 13/13] Set ALSA_CONFIG_PATH in wrapper for GNOME desktop launch GNOME desktop launches don't inherit NixOS shell environment. Without ALSA_CONFIG_PATH, rtmidi2 can't find alsa.conf, fails to create a sequencer client, and pygame segfaults on MIDI init. Co-Authored-By: Claude Opus 4.6 --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 7b64a0a..7324ad8 100644 --- a/flake.nix +++ b/flake.nix @@ -144,6 +144,7 @@ # Wrap to include runtime libraries + set window class for desktop icon matching wrapProgram $out/bin/midimech \ --prefix LD_LIBRARY_PATH : "${runtimeLibs}" \ + --set ALSA_CONFIG_PATH "${pkgs.alsa-lib}/share/alsa/alsa.conf" \ --set SDL_VIDEO_WAYLAND_WMCLASS midimech \ --set SDL_VIDEO_X11_WMCLASS midimech \ --set SDL_APP_ID midimech