#!/usr/bin/env python3
from rtmidi import MidiOut, MidiIn
from time import sleep
from subprocess import run,PIPE
realrun = run
def run(*xs,**kw):
print(f"run({xs=},{kw=})")
return realrun(*xs,**kw)
import re
from collections import defaultdict
import json
# Create classes
# Device class + delegate
# Profile -- delegeate selects profile, activate, and sends all events as but(r,c,x) rbut(r,x) tbut(c,x)
# profile sends back button color info but(r,c,x) etc.
# static method on Launchpad to select color: color(x,brightness) x is 0,1,2,3 for off red green yellow
# brightness os 0..3 for off..1,2,3
# store entire status string when we get it, before parsing it.
mptr = """
t420b: 0 -- ytsets/alexorion/AlexORionAleksandarMarkovic_ProgressiveTalesPodcast.m4a -- [paused] #14/15 33:15/171:11 (19%) vol: 64%
t420b: 15 -- FearAndLoathing/FnlTech_050_DetroitVsBerlin.m4a -- [paused] #1/1 4:50/84:46 (5%) vol: 64%
t420b: 14 -- --stopped--
"""
mpst = """
ytsets/alexorion/AlexORionAleksandarMarkovic_ProgressiveTalesPodcast.m4a
[paused] #14/15 33:15/171:11 (19%)
volume: 64% repeat: on random: on single: off consume: off
> mpc status
"""
# write mp json command to return all statuses, parsed, as json -- write the parser code here
# to parse into a dict struct int => status -- then migrate that code to mp
# See https://pt-a.allsup.co/hh/launchpad/MpdControlIdea1 for idea
class MidiDevice:
def __init__(self,input_port=None,output_port=None,pattern=None,index=0):
self.midi_in = MidiIn()
self.midi_out = MidiOut()
self.input_port = input_port
self.output_port = output_port
self.input_open = False
self.output_open = False
self.set_callback(None)
input_port_candidates = []
output_port_candidates = []
if pattern is not None:
for i,x in enumerate(self.midi_in.get_ports()):
print(i,x)
if pattern in x:
input_port_candidates.append(i)
break
for i,x in enumerate(self.midi_out.get_ports()):
if pattern in x:
output_port_candidates.append(i)
break
if not input_port:
self.input_port = input_port_candidates[index]
if not output_port:
self.output_port = output_port_candidates[index]
def open(self):
if not self.input_open and self.input_port is not None:
print(f"opening input port {self.input_port}")
self.midi_in.open_port(self.input_port)
self.input_open = True
if not self.output_open and self.output_port is not None:
print(f"opening output port {self.output_port}")
self.midi_out.open_port(self.output_port)
self.output_open = True
def __del__(self):
self.close_ports()
def close_ports(self):
if self.input_open:
print(f"closing input port")
self.midi_in.close_port()
if self.output_open:
print(f"closing output port")
self.midi_out.close_port()
def set_callback(self,callback):
self.midi_in.set_callback(callback)
def send_message(self,msg):
self.midi_out.send_message(msg)
def cc(self,n,v):
msg = bytes([0xB0,n,v])
self.send_message(msg)
def on(self,p,v):
msg = bytes([0x90,p,v])
self.send_message(msg)
def off(self,p,v=0):
msg = bytes([0x90,p,0])
self.send_message(msg)
class LpdDevice:
def __init__(self,device):
self.device = device
device.set_callback(self.midi_callback)
delegate = None
device.open()
self.setup()
def set_delegate(self,delegate):
self.delegate = delegate
def make_color(self,idx,br):
idx = idx & 3
br = br & 3
r = (idx&1) * br
g = ((idx&2)>>1) * br
g <<= 4
#print(f"color {idx=} {br=} {r:b=} {g:b=}")
return r | g
def midi_callback(self,x,y):
msg,t = x
print("midi",msg)
if msg[0] & 0xF0 == 0xB0:
print("top")
n,v = msg[1:3]
self.tbut(0,v)
elif msg[0] & 0xF0 == 0x90:
print("rest of pads")
n,v = msg[1:3]
row = (n>>4)
col = (n&0xf)
if col < 8:
self.but(row,col,v)
else:
self.rbut(row,v)
def setup(self):
pass
def tbut(self,c,v):
print("1tbut",c,v)
if self.delegate is not None:
return self.delegate.tbut(c,v)
def rbut(self,r,v):
print("1rbut",r,v)
if self.delegate is not None:
return self.delegate.rbut(r,v)
def but(self,r,c,v):
print("1but",r,c,v)
if self.delegate is not None:
return self.delegate.but(r,c,v)
def set_all(self,idx,br):
for i in range(8):
self.set_tbut(i,idx,br)
self.set_rbut(i,idx,br)
for j in range(8):
self.set_but(i,j,idx,br)
def set_but(self,r,c,idx,br):
color = self.make_color(idx,br)
n = (r << 4) | c
self.device.on(n,color)
def set_tbut(self,c,idx,br):
color = self.make_color(idx,br)
# TODO get
n = 0x68 + c
self.device.cc(n,color)
def set_rbut(self,r,idx,br):
color = self.make_color(idx,br)
return self.set_but(r,8,idx,br)
class LpMpd(LpdDevice):
def setup(self):
print("LpMpd setup")
self.mode = 0 # select, touch-to-play-pause, touch-to-stop
self.ch = 0
self.data = None
self.json = None
self.setup_lower()
def poll_srv(self):
m = run(["mp","json"],stdout=PIPE)
newjson = m.stdout.decode()
if newjson != self.json:
self.json = newjson
self.data = json.loads(newjson)
self.update()
def set_ch(self,ch):
print("set_ch",ch)
if ch != self.ch:
self.ch = ch
self.update_chs()
def cleanup(self):
self.set_all(0)
def color_for_state(self,state):
if state == "stopped":
return 1
if state == "playing":
return 2
if state == "paused":
return 3
return 0
def setup_lower(self):
# row 5 -- seek
self.set_but(4,0,2,3)
self.set_but(4,1,2,2)
self.set_but(4,2,2,1)
self.set_but(4,3,1,1)
self.set_but(4,4,1,1)
self.set_but(4,5,2,1)
self.set_but(4,6,2,2)
self.set_but(4,7,2,3)
# row 6 -- prev/next XX vol -10, -2, 2, 10
self.set_but(5,0,3,1)
self.set_but(5,1,3,1)
self.set_but(5,2,0,0)
self.set_but(5,3,0,0)
self.set_but(5,4,1,3)
self.set_but(5,5,1,1)
self.set_but(5,6,1,1)
self.set_but(5,7,1,3)
self.set_but(7,4,1,3)
self.set_but(7,5,3,3)
self.set_but(7,6,2,3)
self.set_but(7,7,3,3)
def update_chs(self):
#print("update_chs")
for i in range(16):
row = i // 8
col = i % 8
chdata = self.data[i]
state = chdata['playstate']
color = self.color_for_state(state)
br = 3 if i == self.ch else 1
#print("chs:",i,state,row,col,color,br)
self.set_but(row,col,color,br)
def update_ch(self):
# volume bar
chdata = self.data[self.ch]
chvol = chdata['volume']
state = chdata['playstate']
t_pc = chdata['t_pc']
t_pc /= 100.0
t_pc *= 8
t_pc = min(round(t_pc),7)
v = chvol // 10
# 90-100 = 8
# 0-19 = 0
v = min(8,max(0,v-1))
color = self.color_for_state(state)
for j in range(8):
self.set_but(3,j,color,3 if j < v else 0)
if state == "stopped":
self.set_but(2,j,0,0)
else:
self.set_but(2,j,color,3 if j == t_pc else 0)
self.set_but(7,0,1 if chdata["repeat"] else 2,3)
self.set_but(7,1,1 if chdata["single"] else 2,3)
self.set_but(7,2,1 if chdata["random"] else 2,3)
self.set_but(7,3,1 if chdata["consume"] else 2,1)
def update(self):
self.update_chs()
self.update_ch()
def set_ch_but(self,i,idx,br):
row = i // 8
col = i % 8
self.set_but(row,col,idx,br)
def tbut(self,c,v):
pass # ignore
def rbut(self,r,v):
print("2rbut",r,v)
if v == 0:
return
if r == 0:
self.mode = (self.mode + 1) % 3
if self.mode == 0:
self.set_rbut(0,0,0)
if self.mode == 1:
self.set_rbut(0,2,3)
if self.mode == 2:
self.set_rbut(0,1,3)
print("Mode now",self.mode)
# else ignore
def but(self,r,c,v):
print("2but",r,c,v)
if v == 0:
return
if r < 2:
ch = (r << 3) | c
self.set_ch(ch)
if self.mode == 1:
chdata = self.data[ch]
state = chdata['playstate']
if state == "stopped":
self.play(ch)
else:
self.toggle(ch)
elif self.mode == 2:
self.stop(ch)
if r == 3: # volume
newv = 20+(c * 10)
self.set_vol(self.ch,newv)
ch = str(self.ch)
if r == 2:
c = int((c * 100.0/8))
c = str(c)+"%"
run(["mp",ch,",",f"seek {c}"])
if r == 4: # seek
if c == 0:
s = "-10:00"
elif c == 1:
s = "-5:00"
elif c == 2:
s = "-1:00"
elif c == 3:
s = "-0:05"
elif c == 4:
s = "+0:05"
elif c == 5:
s = "+1:00"
elif c == 6:
s = "+5:00"
elif c == 7:
s = "+10:00"
print(f"Seek {s}")
run(["mp",ch,",",f"seek {s}"])
if r == 5:
if c == 0:
run(["mp",ch,",",f"prev"])
elif c == 1:
run(["mp",ch,",",f"next"])
elif c == 2:
pass
elif c == 3:
pass
elif c == 4:
run(["mp",ch,",",f"vol -10"])
elif c == 5:
run(["mp",ch,",",f"vol -2"])
elif c == 6:
run(["mp",ch,",",f"vol +2"])
elif c == 7:
run(["mp",ch,",",f"vol +10"])
if r == 7:
if c == 0:
run(["mp",ch,",",f"repeat"])
elif c == 1:
run(["mp",ch,",",f"single"])
elif c == 2:
run(["mp",ch,",",f"random"])
elif c == 3:
run(["mp",ch,",",f"consume"])
elif c == 4:
self.stop(ch)
elif c == 5:
self.pause(ch)
elif c == 6:
self.play(ch)
elif c == 7:
self.toggle(ch)
def set_vol(self,ch,newvol):
run(["mp",str(ch),",","volume",str(newvol)])
def toggle(self,ch):
run(["mp",str(ch),",","p"])
def stop(self,ch):
run(["mp",str(ch),",","stop"])
def pause(self,ch):
run(["mp",str(ch),",","pause"])
def play(self,ch):
run(["mp",str(ch),",","play"])
midi_device = MidiDevice(pattern="Launchpad")
lp_mpd = LpMpd(midi_device)
try:
while True:
lp_mpd.poll_srv()
sleep(1)
except KeyboardInterrupt:
print("Ctrl-C")
lp_mpd.set_all(0,0)
midi_device.close_ports()