Skip to content

Commit b87631a

Browse files
authored
Merge pull request #3 from quaternionmedia/gld
🎚️ GLD
2 parents 39ef474 + 0bcb211 commit b87631a

File tree

11 files changed

+238
-66
lines changed

11 files changed

+238
-66
lines changed

.github/workflows/draft-release.yml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: 📦 Release
2+
on:
3+
milestone:
4+
types: [closed]
5+
jobs:
6+
release:
7+
name: 📝 Draft Release
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: 📰 Checkout
11+
uses: actions/checkout@v3
12+
13+
- name: 📦 Create draft release from milestone
14+
uses: quaternionmedia/milestones@main

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
remote mixing
44

5-
65
### install
6+
77
install locally
88
`pip install -e ludwig/`
99

1010
### configure
11+
1112
edit `main.py` according to your configuration
1213

1314
### run
15+
1416
`ludwig`

ludwig/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@
99

1010
from pluggy import HookimplMarker
1111

12+
from .mixer import Mixer
13+
from .midi import Midi
14+
1215
mixer = HookimplMarker('mixer')

ludwig/boards/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .qu24 import Qu24
2+
from .gld import Gld

ludwig/boards/gld.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from ludwig import mixer, Mixer, Midi
2+
from rtmidi.midiconstants import NOTE_ON
3+
from ludwig.types import uint1, uint2, uint3, uint4, uint7, uint8
4+
from pydantic import conint
5+
6+
7+
class Gld(Midi, Mixer):
8+
def __init__(self, *args, **kwargs):
9+
super().__init__(self, *args, **kwargs)
10+
self.sysex_header = [0xF0, 0x0, 0x0, 0x1A, 0x50, 0x10, 0x1, 0x0, self.channel]
11+
12+
@mixer
13+
def allCall(self):
14+
self.send(self.sysex_header[:-1] + [0x7F] + [0x10, 0x0, 0xF7])
15+
16+
@mixer
17+
def meters(self):
18+
self.sysex([0x12, 0x1])
19+
20+
@mixer
21+
def fader(self, channel: uint7, volume: uint8):
22+
self.nrpn(channel=channel, param=0x17, data1=volume, data2=0x7)
23+
24+
@mixer
25+
def channel_assign_to_main_mix(self, channel: uint7, on: bool):
26+
self.nrpn(channel=channel, param=0x18, data1=0x7F if on else 0x3F, data2=0x7)
27+
28+
@mixer
29+
def aux_send_level(self, channel: uint7, snd: uint8, level: uint8):
30+
self.nrpn(channel=channel, param=snd, data1=level, data2=0x7)
31+
32+
@mixer
33+
def dca_assign(self, channel: uint7, dca: conint(ge=1, le=16), on: bool):
34+
"""1 indexed"""
35+
self.nrpn(
36+
channel=channel, param=on * 0x40 | dca - 1, data1=0x4 | dca, data2=0x7
37+
)
38+
39+
@mixer
40+
def channel_name(self, channel: uint7, name: str):
41+
if len(name) > 8:
42+
raise Exception("Name must be less than 8 characters")
43+
self.sysex([0x3, channel] + [ord(n) for n in name])
44+
45+
@mixer
46+
def channel_color(self, channel: uint7, color: uint3):
47+
self.sysex([0x6, channel, color])
48+
49+
@mixer
50+
def scene_recall(self, scene: conint(ge=1, le=500)):
51+
"""recall scene, where scenes are 1 indexed"""
52+
self.send([0xB0 | self.channel, 0x0, scene // 128, scene % 128])
53+
54+
@mixer
55+
def mix_select(self, channel: uint7, select: bool):
56+
self.send([0xA0 | self.channel, channel, int(select)])
57+
58+
@mixer
59+
def pan(self, channel: uint7, pan: uint8):
60+
self.nrpn(channel, 0x16, pan, 0x7)
61+
62+
@mixer
63+
def mute(self, channel: uint7):
64+
self.send([NOTE_ON | self.channel, channel, 127])
65+
66+
@mixer
67+
def unmute(self, channel: uint7):
68+
self.send([NOTE_ON | self.channel, channel, 1])
69+
70+
@mixer
71+
def compressor(
72+
self,
73+
channel: uint7,
74+
type: uint2 | None = None,
75+
attack: uint7 | None = None,
76+
release: uint7 | None = None,
77+
knee: uint1 | None = None,
78+
ratio: uint7 | None = None,
79+
threshold: uint7 | None = None,
80+
gain: uint7 | None = None,
81+
):
82+
"""send values to the compressor
83+
84+
Reuqired arguments:
85+
channel (uint7): MIDI channel
86+
87+
Optional arguments:
88+
type (uint2): 4 allowed types
89+
attack (uint7): 300us to 300ms
90+
release (uint7): 100ms to 2s
91+
knee (uint1): 0 = hard, 1 = soft
92+
ratio (uint7): 1:1 to inf (e.g. 2.6:1 = 80)
93+
threshold (uint7): -46 to +18dB
94+
gain (uint7): 0 +18dB
95+
"""
96+
97+
if type:
98+
self.nrpn(channel, 0x61, type, 0x7)
99+
if attack:
100+
self.nrpn(channel, 0x62, attack, 0x7)
101+
if release:
102+
self.nrpn(channel, 0x63, release, 0x7)
103+
if knee:
104+
self.nrpn(channel, 0x64, knee, 0x7)
105+
if ratio:
106+
self.nrpn(channel, 0x65, ratio, 0x7)
107+
if threshold:
108+
self.nrpn(channel, 0x66, threshold, 0x7)
109+
if gain:
110+
self.nrpn(channel, 0x67, gain, 0x7)

ludwig/boards.py ludwig/boards/qu24.py

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
1-
from ludwig import mixer
2-
from ludwig.specs import Midi, Mixer
3-
from rtmidi.midiconstants import NOTE_ON, NOTE_OFF, CONTROL_CHANGE
4-
from ludwig.types import uint1, uint2, uint4, uint7, uint8, uint16
5-
from datetime import datetime
1+
from ludwig import mixer, Midi, Mixer
2+
from rtmidi.midiconstants import NOTE_ON
3+
from ludwig.types import uint1, uint2, uint7, uint8
64

75

86
class Qu24(Midi, Mixer):
97
def __init__(self, *args, **kwargs):
108
super().__init__(self, *args, **kwargs)
11-
self.header = [0xF0, 0x0, 0x0, 0x1A, 0x50, 0x11, 0x1, 0x0, self.channel]
12-
self.start_time = datetime.now()
13-
self.log = []
9+
self.sysex_header = [0xF0, 0x0, 0x0, 0x1A, 0x50, 0x11, 0x1, 0x0, self.channel]
1410

1511
@mixer
1612
def allCall(self):
17-
self.send(self.header[:-1] + [0x7F] + [0x10, 0x0, 0xF7])
13+
self.send(self.sysex_header[:-1] + [0x7F] + [0x10, 0x0, 0xF7])
1814

1915
@mixer
2016
def meters(self):
21-
self.send(self.header + [0x12, 0x1, 0xF7])
17+
self.send(self.sysex_header + [0x12, 0x1, 0xF7])
2218

2319
@mixer
2420
def fader(self, channel: uint7, volume: uint8):
25-
print(self.client_name, 'setting channel volume', channel, volume)
2621
self.nrpn(channel=channel, param=0x17, data1=volume, data2=0x7)
2722

2823
@mixer
@@ -76,5 +71,6 @@ def compressor(
7671
self.nrpn(channel, 0x65, ratio, 0x7)
7772
if threshold:
7873
self.nrpn(channel, 0x66, threshold, 0x7)
74+
7975
if gain:
8076
self.nrpn(channel, 0x67, gain, 0x7)

ludwig/main.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from ludwig.specs import Mixer
2-
from ludwig.boards import Qu24
1+
from .mixer import Mixer
2+
from .boards import Qu24
33
from pluggy import PluginManager
44
from argparse import ArgumentTypeError
55

ludwig/specs.py ludwig/midi.py

+5-50
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,6 @@
1-
from pluggy import HookspecMarker
21
from rtmidi.midiutil import open_midioutput, open_midiinput
32
from rtmidi.midiconstants import CONTROL_CHANGE
4-
from ludwig.types import uint4, uint7, uint8, uint16
5-
6-
mix = HookspecMarker('mixer')
7-
8-
9-
class Mixer:
10-
"""A generic mixer class, to be overwritten by individual boards"""
11-
12-
@mix
13-
def mute(self, channel: int):
14-
"""mute channel"""
15-
16-
@mix
17-
def unmute(self, channel: int):
18-
"""unmute channel"""
19-
20-
@mix
21-
def fader(self, channel: int, volume: int):
22-
"""set the fader volume of a channel"""
23-
24-
@mix
25-
def pan(self, channel: int, pan: int):
26-
"""set pan of the channel"""
27-
28-
@mix
29-
def compressor(
30-
self,
31-
channel: int,
32-
type: int | None = None,
33-
attack: int | None = None,
34-
release: int | None = None,
35-
knee: int | None = None,
36-
ratio: int | None = None,
37-
threshold: int | None = None,
38-
gain: int | None = None,
39-
):
40-
"""set the compressor of the channel"""
41-
42-
@mix
43-
def meters(self):
44-
"""get all meter values"""
45-
46-
@mix
47-
def allCall(self):
48-
"""get full board status"""
49-
50-
@mix
51-
def close(self):
52-
"""close the midi connection"""
3+
from ludwig.types import uint4, uint7, uint8
534

545

556
class Midi:
@@ -97,6 +48,10 @@ def nrpn(self, channel: uint7, param: uint8, data1: uint8, data2: uint8):
9748
self.send([header, 0x6, data1])
9849
self.send([header, 0x26, data2])
9950

51+
def sysex(self, message: list[uint8]):
52+
"""send a MIDI sysex message. Requires self.sysex_header to be set."""
53+
self.midi.send_message([*self.sysex_header, *message, 0xF7])
54+
10055
def __call__(self, event, data=None):
10156
message, deltatime = event
10257
print(self.client_name, message, deltatime)

ludwig/mixer.py

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from pluggy import HookspecMarker
2+
from ludwig.types import uint3, uint7, uint8
3+
from pydantic import conint
4+
5+
mix = HookspecMarker('mixer')
6+
7+
8+
class Mixer:
9+
"""A generic mixer class, to be overwritten by individual boards"""
10+
11+
@mix
12+
def mute(self, channel: int):
13+
"""mute channel"""
14+
15+
@mix
16+
def unmute(self, channel: int):
17+
"""unmute channel"""
18+
19+
@mix
20+
def fader(self, channel: int, volume: int):
21+
"""set the fader volume of a channel"""
22+
23+
@mix
24+
def channel_assign_to_main_mix(self, channel: uint7, on: bool):
25+
"""assign a channel to the main mix"""
26+
27+
@mix
28+
def aux_send_level(self, channel: uint7, snd: uint8, level: uint8):
29+
"""set the aux send level"""
30+
31+
@mix
32+
def dca_assign(self, channel: uint7, dca: conint(ge=1, le=16), on: bool):
33+
"""set the dca assignment for a channel"""
34+
35+
@mix
36+
def channel_name(self, channel: uint7, name: str):
37+
"""set the channel name. Must be less than 8 characters."""
38+
39+
@mix
40+
def channel_color(self, channel: uint7, color: uint3):
41+
"""set the channel color from 8 options:
42+
0: black
43+
1: red
44+
2: green
45+
3: yellow
46+
4: blue
47+
5: purple
48+
6: lt blue
49+
7: white
50+
"""
51+
52+
@mix
53+
def scene_recall(self, scene: conint(ge=1, le=500)):
54+
"""recall scene, where scenes are 1 indexed"""
55+
self.send([0xB0 | self.channel, 0x0, scene // 128, scene % 128])
56+
57+
@mix
58+
def mix_select(self, channel: uint7, select: bool):
59+
self.send([0xA0 | self.channel, channel, int(select)])
60+
61+
@mix
62+
def pan(self, channel: int, pan: int):
63+
"""set pan of the channel"""
64+
65+
@mix
66+
def compressor(
67+
self,
68+
channel: int,
69+
type: int | None = None,
70+
attack: int | None = None,
71+
release: int | None = None,
72+
knee: int | None = None,
73+
ratio: int | None = None,
74+
threshold: int | None = None,
75+
gain: int | None = None,
76+
):
77+
"""set the compressor of the channel"""
78+
79+
@mix
80+
def meters(self):
81+
"""get all meter values"""
82+
83+
@mix
84+
def allCall(self):
85+
"""get full board status"""
86+
87+
@mix
88+
def close(self):
89+
"""close the midi connection"""

ludwig/types.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# unsigned integers
99
uint1 = conint(ge=0, lt=2)
1010
uint2 = conint(ge=0, lt=4)
11+
uint3 = conint(ge=0, lt=8)
1112
uint4 = conint(ge=0, lt=16)
1213
uint7 = conint(ge=0, lt=128)
1314
uint8 = conint(ge=0, lt=256)

setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
setup(
44
name='ludwig',
55
install_requires=[
6-
'pluggy>=0.3,<1.0',
6+
'pluggy>=1.0,<2.0',
77
'python-rtmidi>=1.4.9,<1.5.0',
8-
'pydantic>=1.9.0,<1.10.0',
8+
'pydantic>=1.10.2,<1.11.0',
99
],
1010
entry_points={'console_scripts': ['ludwig=ludwig.main:main']},
1111
packages=find_packages(),

0 commit comments

Comments
 (0)