minimidi 0.1.0__tar.gz

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.
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ rust:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: dtolnay/rust-toolchain@stable
14
+ with:
15
+ components: clippy, rustfmt
16
+ - uses: Swatinem/rust-cache@v2
17
+ - run: cargo fmt --check
18
+ - run: cargo clippy --all-targets -- -D warnings
19
+ - run: cargo test --release
20
+
21
+ python:
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ python-version: ["3.13", "3.14"]
27
+ experimental: [false]
28
+ include:
29
+ # Advance warning if a future Python breaks PyO3 abi3; never
30
+ # blocks the build.
31
+ - python-version: "3.15"
32
+ experimental: true
33
+ continue-on-error: ${{ matrix.experimental }}
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - uses: dtolnay/rust-toolchain@stable
37
+ - uses: Swatinem/rust-cache@v2
38
+ - uses: astral-sh/setup-uv@v6
39
+ - run: uv python install ${{ matrix.python-version }}
40
+ - run: uv sync --all-groups --python ${{ matrix.python-version }}
41
+ - run: uv run maturin develop --release
42
+ - run: uv run ruff check
43
+ - run: uv run ruff format --check
44
+ - run: uv run mypy
45
+ - run: uv run pytest tests -x
46
+
47
+ # No integration job: GitHub's hosted runners use Azure kernels whose
48
+ # linux-modules-extra package does not ship sound modules (snd-virmidi),
49
+ # so the VirMIDI round-trip can never run here. Integration tests run
50
+ # locally / on real hardware via scripts/test.sh with snd-virmidi loaded.
@@ -0,0 +1,61 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ wheels:
9
+ strategy:
10
+ matrix:
11
+ include:
12
+ - runner: ubuntu-latest
13
+ target: x86_64
14
+ - runner: ubuntu-24.04-arm # native aarch64, no QEMU
15
+ target: aarch64
16
+ runs-on: ${{ matrix.runner }}
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: PyO3/maturin-action@v1
20
+ with:
21
+ target: ${{ matrix.target }}
22
+ manylinux: 2_28
23
+ args: --release --out dist
24
+ - uses: astral-sh/setup-uv@v6
25
+ - name: Smoke-test wheel
26
+ run: |
27
+ uv venv --python 3.13 /tmp/smoke
28
+ uv pip install --python /tmp/smoke/bin/python dist/*.whl
29
+ /tmp/smoke/bin/python -c "import minimidi; print(minimidi.__version__)"
30
+ - uses: actions/upload-artifact@v4
31
+ with:
32
+ name: wheels-${{ matrix.target }}
33
+ path: dist/*.whl
34
+
35
+ sdist:
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - uses: PyO3/maturin-action@v1
40
+ with:
41
+ command: sdist
42
+ args: --out dist
43
+ - uses: actions/upload-artifact@v4
44
+ with:
45
+ name: sdist
46
+ path: dist/*.tar.gz
47
+
48
+ publish:
49
+ needs: [wheels, sdist]
50
+ runs-on: ubuntu-latest
51
+ environment: pypi
52
+ permissions:
53
+ id-token: write # PyPI trusted publishing (OIDC) — no API tokens
54
+ steps:
55
+ - uses: actions/download-artifact@v4
56
+ with:
57
+ path: dist
58
+ merge-multiple: true
59
+ - uses: pypa/gh-action-pypi-publish@release/v1
60
+ with:
61
+ packages-dir: dist
@@ -0,0 +1,28 @@
1
+ # Rust / Cargo
2
+ target/
3
+ *.so
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ .mypy_cache/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+
13
+ # Virtual envs (.venv plus scripts/test.sh --python venvs, e.g. .venv-py3.14)
14
+ .venv*/
15
+
16
+ # Build artefacts
17
+ dist/
18
+ build/
19
+ *.egg-info/
20
+
21
+ # uv
22
+ .python-version-cache
23
+
24
+ # Editors / OS
25
+ .idea/
26
+ .vscode/
27
+ .DS_Store
28
+ *.swp
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,45 @@
1
+ # Agent Instructions
2
+
3
+ Mixed Rust + Python project.
4
+
5
+ ## Toolchain
6
+ - Python: `uv` (reads `.python-version`, pins 3.13)
7
+ - Rust: `cargo` (stable) + `maturin` for PyO3 builds
8
+ - Build backend: `maturin` (mixed layout, Rust in `src/`, Python in `python/minimidi/`)
9
+
10
+ ## Setup
11
+ ```
12
+ uv sync --all-groups # creates .venv, installs dev deps
13
+ uv run maturin develop --release # builds Rust ext into the venv
14
+ ```
15
+
16
+ ## Tests
17
+ Run both Rust and Python suites:
18
+ ```
19
+ ./scripts/test.sh # rebuilds ext, runs cargo test + pytest
20
+ ./scripts/test.sh --no-build # skip maturin rebuild (Rust-only changes still need it)
21
+ ```
22
+
23
+ ## File-Scoped Commands
24
+ | Task | Command |
25
+ |------|---------|
26
+ | Python test file | `uv run pytest tests/test_<name>.py` |
27
+ | Rust unit tests | `cargo test` |
28
+ | Rust lint | `cargo clippy -- -D warnings` |
29
+ | Rust format check | `cargo fmt --check` |
30
+ | Python lint | `uv run ruff check python/minimidi tests` |
31
+ | Python typecheck | `uv run mypy python/minimidi tests` |
32
+
33
+ After editing `src/lib.rs`, rerun `uv run maturin develop --release` before `pytest`.
34
+
35
+ ## Layout
36
+ - `src/lib.rs` — Rust extension (PyO3, abi3-py313)
37
+ - `python/minimidi/` — Python wrapper + `.pyi` stubs + `py.typed`
38
+ - `tests/` — Python tests (unit + `tests/integration/` for VirMIDI-dependent)
39
+
40
+ ## Conventions
41
+ - Docstrings: opening `"""` on its own line, content underneath, closing `"""` on its own line.
42
+ - All `.pyi` stubs are hand-written (single source of truth for static types).
43
+ - `mypy --strict` must stay clean.
44
+ - Integration tests under `tests/integration/` are skipped when `snd-virmidi` isn't loaded.
45
+
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
+ (pre-1.0: breaking changes only at minor-version bumps).
8
+
9
+ ## [Unreleased]
10
+
11
+ Nothing yet.
12
+
13
+ ## [0.1.0] - 2026-06-11
14
+
15
+ ### Added
16
+
17
+ - Initial public API: `RawMidiOut` with `note_on()`, `note_off()`,
18
+ `cc()`, `send_bytes()`, `close()`, context-manager support, and `card_id` /
19
+ `device` / `is_open` properties.
20
+ - Card lookup helpers: `list_cards()` and `find_card()`. A host without
21
+ ALSA (no `/proc/asound/cards`) behaves like a host with zero cards.
22
+ - `MinimidiError` exception type; OS-level failures propagate as the
23
+ standard errno-derived built-ins (`FileNotFoundError`, `PermissionError`,
24
+ `BrokenPipeError`, ...).
25
+ - PEP 561 typing: `py.typed` marker and hand-written `_minimidi.pyi` stubs;
26
+ clean under `mypy --strict`.
27
+ - Prebuilt `abi3` wheels (Python ≥3.13) for x86_64 and aarch64 manylinux.
28
+
29
+ [Unreleased]: https://github.com/ModularDevicesMalleableAudio/minimidi/compare/v0.1.0...HEAD
30
+ [0.1.0]: https://github.com/ModularDevicesMalleableAudio/minimidi/releases/tag/v0.1.0
@@ -0,0 +1,45 @@
1
+ # Agent Instructions
2
+
3
+ Mixed Rust + Python project.
4
+
5
+ ## Toolchain
6
+ - Python: `uv` (reads `.python-version`, pins 3.13)
7
+ - Rust: `cargo` (stable) + `maturin` for PyO3 builds
8
+ - Build backend: `maturin` (mixed layout, Rust in `src/`, Python in `python/minimidi/`)
9
+
10
+ ## Setup
11
+ ```
12
+ uv sync --all-groups # creates .venv, installs dev deps
13
+ uv run maturin develop --release # builds Rust ext into the venv
14
+ ```
15
+
16
+ ## Tests
17
+ Run both Rust and Python suites:
18
+ ```
19
+ ./scripts/test.sh # rebuilds ext, runs cargo test + pytest
20
+ ./scripts/test.sh --no-build # skip maturin rebuild (Rust-only changes still need it)
21
+ ```
22
+
23
+ ## File-Scoped Commands
24
+ | Task | Command |
25
+ |------|---------|
26
+ | Python test file | `uv run pytest tests/test_<name>.py` |
27
+ | Rust unit tests | `cargo test` |
28
+ | Rust lint | `cargo clippy -- -D warnings` |
29
+ | Rust format check | `cargo fmt --check` |
30
+ | Python lint | `uv run ruff check python/minimidi tests` |
31
+ | Python typecheck | `uv run mypy python/minimidi tests` |
32
+
33
+ After editing `src/lib.rs`, rerun `uv run maturin develop --release` before `pytest`.
34
+
35
+ ## Layout
36
+ - `src/lib.rs` — Rust extension (PyO3, abi3-py313)
37
+ - `python/minimidi/` — Python wrapper + `.pyi` stubs + `py.typed`
38
+ - `tests/` — Python tests (unit + `tests/integration/` for VirMIDI-dependent)
39
+
40
+ ## Conventions
41
+ - Docstrings: opening `"""` on its own line, content underneath, closing `"""` on its own line.
42
+ - All `.pyi` stubs are hand-written (single source of truth for static types).
43
+ - `mypy --strict` must stay clean.
44
+ - Integration tests under `tests/integration/` are skipped when `snd-virmidi` isn't loaded.
45
+
@@ -0,0 +1,133 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "heck"
7
+ version = "0.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
10
+
11
+ [[package]]
12
+ name = "libc"
13
+ version = "0.2.186"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
16
+
17
+ [[package]]
18
+ name = "minimidi"
19
+ version = "0.1.0"
20
+ dependencies = [
21
+ "pyo3",
22
+ ]
23
+
24
+ [[package]]
25
+ name = "once_cell"
26
+ version = "1.21.4"
27
+ source = "registry+https://github.com/rust-lang/crates.io-index"
28
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
29
+
30
+ [[package]]
31
+ name = "portable-atomic"
32
+ version = "1.13.1"
33
+ source = "registry+https://github.com/rust-lang/crates.io-index"
34
+ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
35
+
36
+ [[package]]
37
+ name = "proc-macro2"
38
+ version = "1.0.106"
39
+ source = "registry+https://github.com/rust-lang/crates.io-index"
40
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
41
+ dependencies = [
42
+ "unicode-ident",
43
+ ]
44
+
45
+ [[package]]
46
+ name = "pyo3"
47
+ version = "0.28.3"
48
+ source = "registry+https://github.com/rust-lang/crates.io-index"
49
+ checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
50
+ dependencies = [
51
+ "libc",
52
+ "once_cell",
53
+ "portable-atomic",
54
+ "pyo3-build-config",
55
+ "pyo3-ffi",
56
+ "pyo3-macros",
57
+ ]
58
+
59
+ [[package]]
60
+ name = "pyo3-build-config"
61
+ version = "0.28.3"
62
+ source = "registry+https://github.com/rust-lang/crates.io-index"
63
+ checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
64
+ dependencies = [
65
+ "target-lexicon",
66
+ ]
67
+
68
+ [[package]]
69
+ name = "pyo3-ffi"
70
+ version = "0.28.3"
71
+ source = "registry+https://github.com/rust-lang/crates.io-index"
72
+ checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
73
+ dependencies = [
74
+ "libc",
75
+ "pyo3-build-config",
76
+ ]
77
+
78
+ [[package]]
79
+ name = "pyo3-macros"
80
+ version = "0.28.3"
81
+ source = "registry+https://github.com/rust-lang/crates.io-index"
82
+ checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
83
+ dependencies = [
84
+ "proc-macro2",
85
+ "pyo3-macros-backend",
86
+ "quote",
87
+ "syn",
88
+ ]
89
+
90
+ [[package]]
91
+ name = "pyo3-macros-backend"
92
+ version = "0.28.3"
93
+ source = "registry+https://github.com/rust-lang/crates.io-index"
94
+ checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
95
+ dependencies = [
96
+ "heck",
97
+ "proc-macro2",
98
+ "pyo3-build-config",
99
+ "quote",
100
+ "syn",
101
+ ]
102
+
103
+ [[package]]
104
+ name = "quote"
105
+ version = "1.0.45"
106
+ source = "registry+https://github.com/rust-lang/crates.io-index"
107
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
108
+ dependencies = [
109
+ "proc-macro2",
110
+ ]
111
+
112
+ [[package]]
113
+ name = "syn"
114
+ version = "2.0.117"
115
+ source = "registry+https://github.com/rust-lang/crates.io-index"
116
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
117
+ dependencies = [
118
+ "proc-macro2",
119
+ "quote",
120
+ "unicode-ident",
121
+ ]
122
+
123
+ [[package]]
124
+ name = "target-lexicon"
125
+ version = "0.13.5"
126
+ source = "registry+https://github.com/rust-lang/crates.io-index"
127
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
128
+
129
+ [[package]]
130
+ name = "unicode-ident"
131
+ version = "1.0.24"
132
+ source = "registry+https://github.com/rust-lang/crates.io-index"
133
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
@@ -0,0 +1,14 @@
1
+ [package]
2
+ name = "minimidi"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ license = "MIT"
6
+ description = "Minimal Linux ALSA Raw MIDI output, with Python bindings via PyO3"
7
+ readme = "README.md"
8
+
9
+ [lib]
10
+ name = "_minimidi" # match #[pymodule] name in lib.rs
11
+ crate-type = ["cdylib"] # dynamic library for Python to load
12
+
13
+ [dependencies]
14
+ pyo3 = { version = "0.28", features = ["extension-module", "abi3-py313"] }
minimidi-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 minimidi authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: minimidi
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Operating System :: POSIX :: Linux
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Rust
11
+ Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
12
+ Classifier: Typing :: Typed
13
+ License-File: LICENSE
14
+ Summary: Minimal Linux ALSA Raw MIDI output, with Python bindings via PyO3
15
+ Keywords: midi,alsa,linux,rust,pyo3
16
+ Author: minimidi authors
17
+ License: MIT
18
+ Requires-Python: >=3.13
19
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
20
+
21
+ # minimidi
22
+
23
+ [![CI](https://github.com/ModularDevicesMalleableAudio/minimidi/actions/workflows/ci.yml/badge.svg)](https://github.com/ModularDevicesMalleableAudio/minimidi/actions/workflows/ci.yml)
24
+
25
+ Minimal Linux ALSA Raw MIDI **output**, written in Rust (PyO3), fully typed for Python 3.13+.
26
+
27
+ `minimidi` does one thing: open an ALSA Raw MIDI device (`/dev/snd/midiC*D*`) write-only
28
+ and send MIDI bytes to it. No input, no sequencer scheduling, no SysEx framing, no
29
+ cross-platform abstraction — just note-on / note-off / CC helpers and a raw-bytes
30
+ escape hatch, shipped as a single `abi3` wheel with no system dependencies.
31
+
32
+ - **Linux only** (ALSA Raw MIDI).
33
+ - **Output only.**
34
+ - **Typed.** PEP 561 `py.typed` + hand-written stubs; clean under `mypy --strict`.
35
+ - **Thread-safe.** One port can be shared across Python threads; writes serialise
36
+ internally and release the GIL across the `write(2)` syscall.
37
+
38
+ See [`AGENTS.md`](./AGENTS.md) for developer / agent workflow instructions.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install minimidi
44
+ # or
45
+ uv add minimidi
46
+ ```
47
+
48
+ Prebuilt `abi3` wheels for x86_64 and aarch64 manylinux (Python ≥3.13) — no
49
+ Rust toolchain or system packages needed. To install from source instead
50
+ (needs Rust stable):
51
+
52
+ ```bash
53
+ pip install git+https://github.com/ModularDevicesMalleableAudio/minimidi
54
+ ```
55
+
56
+ ## Quick start
57
+
58
+ ```python
59
+ import minimidi
60
+
61
+ with minimidi.RawMidiOut("VirMIDI", device=0) as port:
62
+ port.note_on(channel=1, note=60, velocity=100) # middle C on
63
+ port.cc(channel=1, controller=74, value=64) # filter cutoff
64
+ port.note_off(channel=1, note=60) # note off (velocity 0)
65
+ # port auto-closes on context exit
66
+ ```
67
+
68
+ Channels are **1-indexed** (1–16), matching common MIDI convention. Notes,
69
+ velocities, controllers, and values are 0–127. Out-of-range arguments raise
70
+ `MinimidiError` before anything is written.
71
+
72
+ ## Finding your card
73
+
74
+ Ports are addressed by ALSA card id (the bracketed name in `/proc/asound/cards`)
75
+ plus a raw device number:
76
+
77
+ ```python
78
+ import minimidi
79
+
80
+ minimidi.list_cards()
81
+ # [(0, "PCH"), (1, "VirMIDI"), (3, "pisound")]
82
+
83
+ minimidi.find_card("pisound")
84
+ # 3 — raises MinimidiError if not present
85
+
86
+ port = minimidi.RawMidiOut("pisound", device=0)
87
+ # opens /dev/snd/midiC3D0 write-only
88
+ ```
89
+
90
+ Card-id matching is exact and case-sensitive (ALSA's bracket padding is trimmed).
91
+ `device` is typically 0–3 for `snd-virmidi`; a non-existent device raises
92
+ `FileNotFoundError` at open time.
93
+
94
+ ## Escape hatch: raw bytes
95
+
96
+ Messages without a first-class helper (pitch bend, program change, aftertouch, ...)
97
+ can be written verbatim:
98
+
99
+ ```python
100
+ channel = 1
101
+ port.send_bytes(bytes([0xE0 | (channel - 1), 0x00, 0x40])) # pitch bend, centre
102
+ ```
103
+
104
+ `send_bytes` accepts only `bytes` (not `bytearray`/`memoryview`/`str`), length
105
+ 1–1024, and performs **no MIDI semantic validation** — the caller owns correctness.
106
+
107
+ ## Errors and recovery
108
+
109
+ | Condition | Exception |
110
+ |---|---|
111
+ | Unknown card id, out-of-range argument, write on closed port | `MinimidiError` |
112
+ | Device node missing, no permission, device disappeared mid-write | `FileNotFoundError` / `PermissionError` / `BrokenPipeError` (standard errno-derived `OSError` subclasses) |
113
+ | Non-`bytes` payload to `send_bytes` | `TypeError` |
114
+
115
+ A failed write does **not** close the port: `is_open` means "not explicitly
116
+ closed", and a retry raises the same OS error again. After device loss (USB
117
+ hot-replug, module reload), construct a *new* `RawMidiOut` — that re-resolves the
118
+ card number from `/proc/asound/cards`, which is correct when the kernel may have
119
+ re-numbered the card. Closed is final: there is no `open()`/reopen method.
120
+
121
+ ## Testing without hardware
122
+
123
+ Load the kernel's virtual Raw MIDI card and `minimidi` can talk to it like any
124
+ other device:
125
+
126
+ ```bash
127
+ sudo modprobe snd-virmidi # persistent: echo snd-virmidi | sudo tee /etc/modules-load.d/virmidi.conf
128
+ ```
129
+
130
+ ```python
131
+ port = minimidi.RawMidiOut("VirMIDI", device=0)
132
+ ```
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ uv sync --all-groups # venv + dev deps
138
+ uv run maturin develop --release # build the Rust extension into the venv
139
+ ./scripts/test.sh # cargo test + ruff + mypy + pytest
140
+ ```
141
+
142
+ Integration tests (`tests/integration/`) need `snd-virmidi` loaded and `aconnect`
143
+ (alsa-utils) installed; they skip cleanly otherwise.
144
+
145
+ ## License
146
+
147
+ MIT — see [`LICENSE`](./LICENSE).
148
+