Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ midimech.spec
build/
dist/
.idea/
result
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,38 @@ 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
```

**NixOS module (recommended):**

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:
- Desktop entry with icon (shows in GNOME, KDE, etc.)
- Virtual MIDI cable — equivalent to loopMIDI on Windows, started automatically
- 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.

### Mac, Linux, and Running from Git

- Download the project by typing the following commands in terminal:
Expand Down
27 changes: 27 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

209 changes: 209 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
{
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";
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: starts virtual MIDI cable + midimech
cat > $out/bin/midimech <<LAUNCHER
#!/bin/sh
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
cd $out/share/midimech
exec ${pythonEnv}/bin/python3 $out/share/midimech/midimech.py "\$@"
LAUNCHER
chmod +x $out/bin/midimech
patchShebangs $out/bin/midimech

# 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

# 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 <<DESKTOP
[Desktop Entry]
Name=Midimech
Comment=Isomorphic musical layout engine for LinnStrument and Launchpad X
Exec=$out/bin/midimech
Icon=midimech
Terminal=false
Type=Application
Categories=Audio;Music;Midi;
StartupWMClass=midimech
DESKTOP

'';

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" ];
};
};
};
}
73 changes: 73 additions & 0 deletions midimech-vport.c
Original file line number Diff line number Diff line change
@@ -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 <alsa/asoundlib.h>
#include <signal.h>
#include <stdio.h>

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;
}
Loading