linux-midi-latch 1.0.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.
- linux_midi_latch-1.0.0/.gitignore +4 -0
- linux_midi_latch-1.0.0/LICENSE +21 -0
- linux_midi_latch-1.0.0/PKG-INFO +46 -0
- linux_midi_latch-1.0.0/README.md +27 -0
- linux_midi_latch-1.0.0/linux_midi_latch/__init__.py +0 -0
- linux_midi_latch-1.0.0/linux_midi_latch/main.py +149 -0
- linux_midi_latch-1.0.0/pyproject.toml +26 -0
- linux_midi_latch-1.0.0/release +47 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 @readwith
|
|
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,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linux-midi-latch
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MIDI note latch for Linux using mididings
|
|
5
|
+
Project-URL: Homepage, https://github.com/talwrii/linux-midi-latch
|
|
6
|
+
Project-URL: Repository, https://github.com/talwrii/linux-midi-latch
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: latch,linux,midi,mididings,music
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Requires-Dist: mido
|
|
17
|
+
Requires-Dist: python-rtmidi
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# linux midi latch
|
|
21
|
+
Implement latch functionality where each key acts as a toggle for a note for midi devices on linux. Optionally switch this functionality on and off with the systain pedal. Uses mido, only works on linux
|
|
22
|
+
|
|
23
|
+
## Alternatives and prior work
|
|
24
|
+
Some hardware implements latching. Apparently Nord Lead A1, Sequential Prophet 6 and Novation Summit amongt others.
|
|
25
|
+
|
|
26
|
+
mido gives you complete control over what midi doers so you can do what you want if you are willing to put in the work - which has become fairly cheap with LLMs. Apparently, the VST Bidule has a midi latch feature as does VCV rack. These will work on windows. `loopMIDI` combined with mido apparently gives you programmatic contorl on windows. I have not tried most of thse things.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
pipx install linux-midi-latch
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
`linux-midi-latch --alsa`
|
|
33
|
+
|
|
34
|
+
Use `aconnect` to route midi traffic through linux-midi-latch.
|
|
35
|
+
|
|
36
|
+
There are --jack and --osc options. I have not tested them nor do I know how to use them, but mididings works with jthem.
|
|
37
|
+
|
|
38
|
+
## My setup
|
|
39
|
+
I am using this with an impact gxp6 nektar on Ubuntu LTS with a yamaha sustain pedel using alsa midi and fluidsynth.
|
|
40
|
+
|
|
41
|
+
## About me
|
|
42
|
+
I am @readwith. I make tools for reading and agency and research - with and without Obsidian and AI.
|
|
43
|
+
You can follow me https::/readwithai.substack.com/ and https://x.com/readwithai
|
|
44
|
+
|
|
45
|
+
I also mess around with music and fitness technology. I have a treadmill desk with a lot of musical instruments. See [r/musicaltreadmilldesk](https://www.reddit.com/r/musicaltreadmilldesk/) for this. If you like this sort of tool you may well want to follow me there.
|
|
46
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# linux midi latch
|
|
2
|
+
Implement latch functionality where each key acts as a toggle for a note for midi devices on linux. Optionally switch this functionality on and off with the systain pedal. Uses mido, only works on linux
|
|
3
|
+
|
|
4
|
+
## Alternatives and prior work
|
|
5
|
+
Some hardware implements latching. Apparently Nord Lead A1, Sequential Prophet 6 and Novation Summit amongt others.
|
|
6
|
+
|
|
7
|
+
mido gives you complete control over what midi doers so you can do what you want if you are willing to put in the work - which has become fairly cheap with LLMs. Apparently, the VST Bidule has a midi latch feature as does VCV rack. These will work on windows. `loopMIDI` combined with mido apparently gives you programmatic contorl on windows. I have not tried most of thse things.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
pipx install linux-midi-latch
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
`linux-midi-latch --alsa`
|
|
14
|
+
|
|
15
|
+
Use `aconnect` to route midi traffic through linux-midi-latch.
|
|
16
|
+
|
|
17
|
+
There are --jack and --osc options. I have not tested them nor do I know how to use them, but mididings works with jthem.
|
|
18
|
+
|
|
19
|
+
## My setup
|
|
20
|
+
I am using this with an impact gxp6 nektar on Ubuntu LTS with a yamaha sustain pedel using alsa midi and fluidsynth.
|
|
21
|
+
|
|
22
|
+
## About me
|
|
23
|
+
I am @readwith. I make tools for reading and agency and research - with and without Obsidian and AI.
|
|
24
|
+
You can follow me https::/readwithai.substack.com/ and https://x.com/readwithai
|
|
25
|
+
|
|
26
|
+
I also mess around with music and fitness technology. I have a treadmill desk with a lot of musical instruments. See [r/musicaltreadmilldesk](https://www.reddit.com/r/musicaltreadmilldesk/) for this. If you like this sort of tool you may well want to follow me there.
|
|
27
|
+
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
linux-midi-latch - MIDI note latch for Linux using mido.
|
|
4
|
+
|
|
5
|
+
Each note press toggles that note on/off (latch mode).
|
|
6
|
+
|
|
7
|
+
Sustain pedal modes:
|
|
8
|
+
default -- latch always on; first sustain touch switches to --sustain-pedal mode
|
|
9
|
+
--ignore-sustain -- latch always on, sustain passed through untouched
|
|
10
|
+
--sustain-pedal -- sustain pedal must be held for latch to be active
|
|
11
|
+
--sustain-toggles -- sustain pedal press toggles latch on/off
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import mido
|
|
16
|
+
import mido.backends.rtmidi # noqa
|
|
17
|
+
|
|
18
|
+
latched_notes = set()
|
|
19
|
+
latch_active = True
|
|
20
|
+
sustain_down = False
|
|
21
|
+
sustain_mode = None
|
|
22
|
+
|
|
23
|
+
def note_off_msg(channel, note):
|
|
24
|
+
return mido.Message('note_off', channel=channel, note=note, velocity=0)
|
|
25
|
+
|
|
26
|
+
def note_on_msg(channel, note, velocity):
|
|
27
|
+
return mido.Message('note_on', channel=channel, note=note, velocity=velocity)
|
|
28
|
+
|
|
29
|
+
def process_note(msg, outport):
|
|
30
|
+
if not latch_active:
|
|
31
|
+
outport.send(msg)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
note = msg.note
|
|
35
|
+
channel = msg.channel
|
|
36
|
+
|
|
37
|
+
if msg.type == 'note_on' and msg.velocity > 0:
|
|
38
|
+
if note in latched_notes:
|
|
39
|
+
latched_notes.discard(note)
|
|
40
|
+
outport.send(note_off_msg(channel, note))
|
|
41
|
+
else:
|
|
42
|
+
latched_notes.add(note)
|
|
43
|
+
outport.send(note_on_msg(channel, note, msg.velocity))
|
|
44
|
+
# swallow note_off — latch holds the note
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def process_sustain_default(msg, outport):
|
|
48
|
+
global sustain_mode, latch_active, sustain_down
|
|
49
|
+
|
|
50
|
+
pedal_down = msg.value >= 64
|
|
51
|
+
|
|
52
|
+
if sustain_mode == 'default':
|
|
53
|
+
print("Sustain pedal detected — switching to sustain-pedal mode")
|
|
54
|
+
sustain_mode = 'pedal'
|
|
55
|
+
|
|
56
|
+
sustain_down = pedal_down
|
|
57
|
+
latch_active = pedal_down
|
|
58
|
+
if pedal_down:
|
|
59
|
+
print("Pedal down — latch on")
|
|
60
|
+
else:
|
|
61
|
+
print("Pedal released — latch off (notes keep ringing)")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def process_sustain_pedal(msg, outport):
|
|
65
|
+
global latch_active, sustain_down
|
|
66
|
+
|
|
67
|
+
pedal_down = msg.value >= 64
|
|
68
|
+
sustain_down = pedal_down
|
|
69
|
+
latch_active = pedal_down
|
|
70
|
+
|
|
71
|
+
if pedal_down:
|
|
72
|
+
print("Pedal down — latch on")
|
|
73
|
+
else:
|
|
74
|
+
print("Pedal released — latch off (notes keep ringing)")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def process_sustain_toggles(msg, outport):
|
|
78
|
+
global latch_active
|
|
79
|
+
|
|
80
|
+
if msg.value >= 64:
|
|
81
|
+
latch_active = not latch_active
|
|
82
|
+
print(f"Latch {'on' if latch_active else 'off'}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
global sustain_mode
|
|
87
|
+
|
|
88
|
+
parser = argparse.ArgumentParser(
|
|
89
|
+
prog='linux-midi-latch',
|
|
90
|
+
description='MIDI note latch for Linux. Route MIDI through this with aconnect.',
|
|
91
|
+
epilog='example: linux-midi-latch --alsa',
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
backend_group = parser.add_mutually_exclusive_group(required=True)
|
|
95
|
+
backend_group.add_argument('--alsa', action='store_true', help='Use ALSA MIDI backend')
|
|
96
|
+
backend_group.add_argument('--jack', action='store_true', help='Use JACK MIDI backend')
|
|
97
|
+
|
|
98
|
+
sustain_group = parser.add_mutually_exclusive_group()
|
|
99
|
+
sustain_group.add_argument('--ignore-sustain', action='store_true',
|
|
100
|
+
help='Latch always on, sustain pedal passed through untouched')
|
|
101
|
+
sustain_group.add_argument('--sustain-pedal', action='store_true',
|
|
102
|
+
help='Sustain pedal must be held for latch to be active')
|
|
103
|
+
sustain_group.add_argument('--sustain-toggles', action='store_true',
|
|
104
|
+
help='Sustain pedal press toggles latch on/off')
|
|
105
|
+
|
|
106
|
+
args = parser.parse_args()
|
|
107
|
+
|
|
108
|
+
if args.ignore_sustain:
|
|
109
|
+
sustain_mode = 'ignore'
|
|
110
|
+
sustain_fn = None
|
|
111
|
+
print("Latch on. Sustain pedal ignored.")
|
|
112
|
+
elif args.sustain_pedal:
|
|
113
|
+
sustain_mode = 'pedal'
|
|
114
|
+
sustain_fn = process_sustain_pedal
|
|
115
|
+
print("Latch active while sustain pedal is held.")
|
|
116
|
+
elif args.sustain_toggles:
|
|
117
|
+
sustain_mode = 'toggles'
|
|
118
|
+
sustain_fn = process_sustain_toggles
|
|
119
|
+
print("Latch on. Sustain pedal toggles latch.")
|
|
120
|
+
else:
|
|
121
|
+
sustain_mode = 'default'
|
|
122
|
+
sustain_fn = process_sustain_default
|
|
123
|
+
print("Latch on. First sustain touch will switch to sustain-pedal mode.")
|
|
124
|
+
|
|
125
|
+
if args.alsa:
|
|
126
|
+
mido.set_backend('mido.backends.rtmidi/LINUX_ALSA')
|
|
127
|
+
else:
|
|
128
|
+
mido.set_backend('mido.backends.rtmidi/UNIX_JACK')
|
|
129
|
+
|
|
130
|
+
print(f"Backend: {'alsa' if args.alsa else 'jack'}. Use aconnect to route MIDI through linux-midi-latch.")
|
|
131
|
+
|
|
132
|
+
with mido.open_input('linux-midi-latch', virtual=True) as inport, \
|
|
133
|
+
mido.open_output('linux-midi-latch out', virtual=True) as outport:
|
|
134
|
+
|
|
135
|
+
print("Ready.")
|
|
136
|
+
for msg in inport:
|
|
137
|
+
if msg.type in ('note_on', 'note_off'):
|
|
138
|
+
process_note(msg, outport)
|
|
139
|
+
elif msg.type == 'control_change' and msg.control == 64:
|
|
140
|
+
if sustain_fn:
|
|
141
|
+
sustain_fn(msg, outport)
|
|
142
|
+
else:
|
|
143
|
+
outport.send(msg)
|
|
144
|
+
else:
|
|
145
|
+
outport.send(msg)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == '__main__':
|
|
149
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "hatchling",]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "linux-midi-latch"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "MIDI note latch for Linux using mididings"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = [ "mido", "python-rtmidi",]
|
|
12
|
+
keywords = [ "midi", "latch", "linux", "mididings", "music",]
|
|
13
|
+
classifiers = [ "Environment :: Console", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Topic :: Multimedia :: Sound/Audio :: MIDI",]
|
|
14
|
+
|
|
15
|
+
[project.license]
|
|
16
|
+
text = "MIT"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
linux-midi-latch = "linux_midi_latch.main:main"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/talwrii/linux-midi-latch"
|
|
23
|
+
Repository = "https://github.com/talwrii/linux-midi-latch"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = [ "linux_midi_latch",]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -o errexit
|
|
3
|
+
set -o nounset
|
|
4
|
+
set -o pipefail
|
|
5
|
+
|
|
6
|
+
# SSH ControlMaster setup
|
|
7
|
+
CONTROL_PATH="/tmp/ssh-release-%r@%h:%p"
|
|
8
|
+
|
|
9
|
+
echo "🔐 Establishing SSH connection to GitHub..."
|
|
10
|
+
ssh -o ControlMaster=yes -o ControlPath="$CONTROL_PATH" -o ControlPersist=300 -fN git@github.com
|
|
11
|
+
|
|
12
|
+
# Use ControlMaster for git
|
|
13
|
+
export GIT_SSH_COMMAND="ssh -o ControlPath=$CONTROL_PATH"
|
|
14
|
+
|
|
15
|
+
# Fail if there are any modified files
|
|
16
|
+
if ! git diff-index --quiet HEAD --; then
|
|
17
|
+
echo "Error: You have uncommitted changes to tracked files"
|
|
18
|
+
git status --short
|
|
19
|
+
ssh -o ControlPath="$CONTROL_PATH" -O exit git@github.com 2>/dev/null || true
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
git push
|
|
24
|
+
|
|
25
|
+
if [ -f pyproject.toml ]; then
|
|
26
|
+
VERSION=$(cat pyproject.toml | toml2json | jq -rc '.project.version')
|
|
27
|
+
else
|
|
28
|
+
VERSION=$(python3 setup.py --version)
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
git tag "$VERSION"
|
|
32
|
+
git push --tags
|
|
33
|
+
|
|
34
|
+
rm -rf dist *.egg-info build
|
|
35
|
+
|
|
36
|
+
if [ -f pyproject.toml ]; then
|
|
37
|
+
python3 -m build
|
|
38
|
+
else
|
|
39
|
+
python3 setup.py sdist
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
twine upload -p $(cat token) dist/*
|
|
43
|
+
|
|
44
|
+
# Close ControlMaster
|
|
45
|
+
ssh -o ControlPath="$CONTROL_PATH" -O exit git@github.com 2>/dev/null || true
|
|
46
|
+
|
|
47
|
+
echo "✅ Released version $VERSION"
|