466 lines
8.8 KiB
Python
466 lines
8.8 KiB
Python
# Guinea synth
|
|
# -*- coding: utf8 -*-
|
|
#
|
|
# built by Severák in awful year 2020
|
|
#
|
|
# feel free to use in any way you like
|
|
# because it's WTFPL licensed
|
|
# but remember this is built on some GNU-licensed code, so respect it
|
|
#
|
|
|
|
# PIN numbers of rotary encoder - change these if needed:
|
|
PIN_clk = 10
|
|
PIN_dt = 9
|
|
PIN_btn = 25
|
|
|
|
import sys
|
|
import time
|
|
from ctcsoundSession import CsoundSession
|
|
|
|
def clip(value, lower, upper):
|
|
return lower if value < lower else upper if value > upper else value
|
|
|
|
# menu params
|
|
|
|
class act_menu:
|
|
params = []
|
|
current = 0
|
|
|
|
def __init__(self):
|
|
self.params = []
|
|
|
|
def add(self, param):
|
|
self.params.append(param)
|
|
|
|
def enter(self):
|
|
self.preview()
|
|
|
|
def up(self):
|
|
self.current = (self.current + 1) % len(self.params)
|
|
self.preview()
|
|
|
|
def down(self):
|
|
self.current = (self.current - 1) % len(self.params)
|
|
self.preview()
|
|
|
|
def preview(self):
|
|
self.params[self.current].preview()
|
|
|
|
def push(self):
|
|
self.params[self.current].enter()
|
|
menu(self.params[self.current])
|
|
|
|
class act_select:
|
|
label = ''
|
|
params = []
|
|
current = 0
|
|
callback = None
|
|
|
|
def __init__(self, label, params, callback):
|
|
self.params=params
|
|
self.callback=callback
|
|
self.label = label
|
|
|
|
def enter(self):
|
|
cls()
|
|
echo(self.label)
|
|
echo('> {}'.format(self.params[self.current]), 1)
|
|
|
|
def up(self):
|
|
self.current = (self.current + 1) % len(self.params)
|
|
self.enter()
|
|
|
|
def down(self):
|
|
self.current = (self.current - 1) % len(self.params)
|
|
self.enter()
|
|
|
|
def preview(self):
|
|
cls()
|
|
echo(self.label)
|
|
echo('{}'.format(self.params[self.current]), 1)
|
|
|
|
def push(self):
|
|
self.callback(self.current, self.params[self.current])
|
|
|
|
class act_confirm:
|
|
value = 0
|
|
prev = None
|
|
question = ""
|
|
callback = None
|
|
|
|
def __init__(self, prev, question, callback):
|
|
self.prev = prev
|
|
self.question=question
|
|
self.callback = callback
|
|
|
|
def preview(self):
|
|
cls()
|
|
echo(self.question)
|
|
echo("no", 1)
|
|
|
|
def enter(self):
|
|
cls()
|
|
echo(self.question)
|
|
echo("> yes" if self.value else "> no", 1)
|
|
|
|
def up(self):
|
|
self.value = 1 if (self.value==0) else 0
|
|
self.enter()
|
|
|
|
def down(self):
|
|
self.up()
|
|
|
|
def push(self):
|
|
if self.value==1:
|
|
self.callback();
|
|
else:
|
|
self.prev.preview()
|
|
menu(self.prev)
|
|
|
|
|
|
# CSD params
|
|
|
|
class csd_param:
|
|
value = 0
|
|
param_name = ''
|
|
label = ''
|
|
preset = 1
|
|
min = 0
|
|
max = 1
|
|
step = 1
|
|
csd = None
|
|
prev = None
|
|
|
|
def __init__(self, prev, csd, param_name, label, preset=1, min=0, max=1, per_round=20.0):
|
|
self.prev = prev
|
|
self.csd = csd
|
|
self.param_name = param_name
|
|
self.label = label
|
|
self.preset = preset
|
|
self.value = preset
|
|
self.min = min
|
|
self.max = max
|
|
self.step = (max - min) / per_round
|
|
csd.setControlChannel(param_name, preset)
|
|
|
|
def preview(self):
|
|
cls()
|
|
echo(self.label)
|
|
echo("{}".format(self.value), 1)
|
|
|
|
def enter(self):
|
|
cls()
|
|
echo(self.label)
|
|
echo("> {}".format(self.value), 1)
|
|
|
|
|
|
def up(self):
|
|
self.value = clip(self.value + self.step, self.min, self.max)
|
|
self.csd.setControlChannel(self.param_name, self.value)
|
|
self.enter()
|
|
|
|
def down(self):
|
|
self.value = clip(self.value - self.step, self.min, self.max)
|
|
self.csd.setControlChannel(self.param_name, self.value)
|
|
self.enter()
|
|
|
|
def push(self):
|
|
self.preview()
|
|
menu(self.prev)
|
|
|
|
class csd_select:
|
|
value = 0
|
|
param_name = ''
|
|
label = ''
|
|
preset = 1
|
|
params = []
|
|
csd = None
|
|
prev = None
|
|
|
|
def __init__(self, prev, csd, param_name, label, preset=0, params=[]):
|
|
self.prev = prev
|
|
self.csd = csd
|
|
self.param_name = param_name
|
|
self.label = label
|
|
self.preset = preset
|
|
self.value = preset
|
|
self.params = params
|
|
csd.setControlChannel(param_name, preset+1)
|
|
|
|
def preview(self):
|
|
cls()
|
|
echo(self.label)
|
|
echo("{}".format(self.params[self.value]), 1)
|
|
|
|
def enter(self):
|
|
cls()
|
|
echo(self.label)
|
|
echo("> {}".format(self.params[self.value]), 1)
|
|
|
|
def up(self):
|
|
self.value = (self.value + 1) % len(self.params)
|
|
self.csd.setControlChannel(self.param_name, self.value+1)
|
|
self.enter()
|
|
|
|
def down(self):
|
|
self.value = (self.value + 1) % len(self.params)
|
|
self.csd.setControlChannel(self.param_name, self.value+1)
|
|
self.enter()
|
|
|
|
def push(self):
|
|
self.preview()
|
|
menu(self.prev)
|
|
|
|
class csd_checkbox:
|
|
value = 0
|
|
param_name = ''
|
|
label = ''
|
|
preset = 1
|
|
csd = None
|
|
prev = None
|
|
|
|
def __init__(self, prev, csd, param_name, label, preset=0):
|
|
self.prev = prev
|
|
self.csd = csd
|
|
self.param_name = param_name
|
|
self.label = label
|
|
self.preset = preset
|
|
self.value = preset
|
|
csd.setControlChannel(param_name, preset)
|
|
|
|
def preview(self):
|
|
cls()
|
|
echo(self.label)
|
|
echo("yes" if self.value else "no", 1)
|
|
|
|
def enter(self):
|
|
cls()
|
|
echo(self.label)
|
|
echo("> yes" if self.value else "> no", 1)
|
|
|
|
|
|
def up(self):
|
|
self.value = 1 if (self.value==0) else 0
|
|
self.csd.setControlChannel(self.param_name, self.value)
|
|
self.enter()
|
|
|
|
def down(self):
|
|
self.up()
|
|
|
|
def push(self):
|
|
self.preview()
|
|
menu(self.prev)
|
|
|
|
|
|
# debug classes
|
|
class odpalovac:
|
|
value = 440
|
|
prev = None
|
|
csd = None
|
|
|
|
def __init__(self, prev, csd):
|
|
self.prev = prev
|
|
self.csd = csd
|
|
|
|
def enter(self):
|
|
self.preview()
|
|
|
|
def preview(self):
|
|
echo("Play note:")
|
|
echo("{} Hz".format(self.value), 1)
|
|
|
|
def up(self):
|
|
self.value = clip(self.value +1, 20, 22000)
|
|
self.show()
|
|
|
|
def down(self):
|
|
self.value = clip(self.value -1, 20, 22000)
|
|
self.show()
|
|
|
|
def push(self):
|
|
self.csd.note([1,0,0.5,self.value])
|
|
self.prev.preview()
|
|
menu(self.prev)
|
|
|
|
|
|
class hladina:
|
|
|
|
value = 0
|
|
prev = None
|
|
name = ""
|
|
|
|
def __init__(self, prev, name):
|
|
self.prev = prev
|
|
self.name = name
|
|
|
|
def enter(self):
|
|
self.preview()
|
|
|
|
def preview(self):
|
|
cls()
|
|
echo(self.name)
|
|
echo("{}".format(self.value), 1)
|
|
|
|
def up(self):
|
|
self.value += 1
|
|
self.show()
|
|
|
|
def down(self):
|
|
self.value -= 1
|
|
self.show()
|
|
|
|
def push(self):
|
|
self.prev.preview()
|
|
menu(self.prev)
|
|
|
|
|
|
|
|
|
|
# main
|
|
_rotary = None
|
|
|
|
print("Guinea synth")
|
|
if "--tkinter" in sys.argv:
|
|
print("(using Tkinter emulation instead of hardware)")
|
|
|
|
import Tkinter as tk
|
|
|
|
window = tk.Tk()
|
|
window.geometry("200x150")
|
|
rows = [None, None]
|
|
rows[0] = tk.StringVar()
|
|
tk.Label(text="", textvariable=rows[0]).pack()
|
|
rows[1] = tk.StringVar()
|
|
tk.Label(text="", textvariable=rows[1]).pack()
|
|
tk.Button(text="UP", command=lambda: _rotary.up()).pack()
|
|
tk.Button(text="ENTER", command=lambda: _rotary.push()).pack()
|
|
tk.Button(text="DOWN", command=lambda: _rotary.down()).pack()
|
|
|
|
|
|
def cls():
|
|
rows[0].set("")
|
|
rows[1].set("")
|
|
|
|
def echo(text, row=0):
|
|
rows[row].set(text)
|
|
|
|
def menu(callback):
|
|
global _rotary
|
|
if not _rotary:
|
|
_rotary = callback
|
|
window.mainloop()
|
|
else:
|
|
_rotary = callback
|
|
|
|
|
|
else:
|
|
print("(using hardware controls)")
|
|
import lcddriver
|
|
_display = lcddriver.lcd()
|
|
|
|
def cls():
|
|
_display.lcd_clear()
|
|
|
|
def echo(text, row=0):
|
|
_display.lcd_display_string(text, row+1)
|
|
|
|
from RPi import GPIO
|
|
GPIO.setmode(GPIO.BCM)
|
|
GPIO.setup(PIN_clk, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
|
|
GPIO.setup(PIN_dt, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
|
|
GPIO.setup(PIN_btn, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
|
|
|
clkLastState = GPIO.input(PIN_clk)
|
|
btnLastState = GPIO.input(PIN_btn)
|
|
|
|
|
|
def _pull_events():
|
|
global clkLastState
|
|
global btnLastState
|
|
while True:
|
|
btnPushed = GPIO.input(PIN_btn)
|
|
if ((not btnLastState) and btnPushed):
|
|
_rotary.push()
|
|
time.sleep(0.05)
|
|
else:
|
|
clkState = GPIO.input(PIN_clk)
|
|
dtState = GPIO.input(PIN_dt)
|
|
if clkState != clkLastState:
|
|
if dtState != clkState:
|
|
_rotary.up()
|
|
else:
|
|
_rotary.down();
|
|
clkLastState = clkState
|
|
btnLastState = btnPushed
|
|
|
|
def menu(callback):
|
|
global _rotary
|
|
if not _rotary:
|
|
_rotary = callback
|
|
_pull_events()
|
|
else:
|
|
_rotary = callback
|
|
|
|
|
|
######
|
|
|
|
def load_csd(idx, csd_path):
|
|
global csd
|
|
print(csd_path)
|
|
menu_constructor = []
|
|
in_def = False
|
|
with open(csd_path) as csdf:
|
|
lines = csdf.readlines()
|
|
for line in lines:
|
|
if line.strip()=='</GuineaSynth>':
|
|
in_def = False
|
|
if in_def:
|
|
menu_constructor.append(line)
|
|
if line.strip()=='<GuineaSynth>':
|
|
in_def = True
|
|
|
|
cls()
|
|
echo("reseting synth...")
|
|
echo("please wait...", 1)
|
|
csd.resetSession(csd_path)
|
|
print("### params debug: ###")
|
|
print(''.join(menu_constructor))
|
|
print("###")
|
|
exec(''.join(menu_constructor))
|
|
cls()
|
|
echo(csd_path)
|
|
echo("READY", 1)
|
|
|
|
try:
|
|
import glob
|
|
|
|
csd = CsoundSession()
|
|
|
|
if sys.platform=='win32':
|
|
csd.setOption('-+rtmidi=winmme')
|
|
csd.setOption('-Ma')
|
|
csd.setOption('-+rtaudio=portaudio')
|
|
csd.setOption('-odac')
|
|
csd.setOption('-b 4096')
|
|
# TODO - fix latency on Windows. But how?
|
|
|
|
# print(csd)
|
|
|
|
# print(csd.audioDevList(True))
|
|
# print(csd.csdFileName())
|
|
|
|
csd_list = glob.glob("*.csd")
|
|
|
|
system_menu = act_menu()
|
|
system_menu.add(act_confirm(system_menu, 'Shutdown?', exit))
|
|
cls()
|
|
echo("Guinea synth")
|
|
echo("READY", 1)
|
|
menu(act_select('Select synth:', csd_list, load_csd))
|
|
print("OK")
|
|
finally:
|
|
cls()
|
|
csd.stopPerformance()
|
|
del csd
|
|
if not "--tkinter" in sys.argv:
|
|
GPIO.cleanup() |