|
- from pythonosc import udp_client
- import curses
- import random
- import sys
- import re
- import time
-
- ESCAPE = 27
- LETTERS = range(97, 123)
- NUMBERS = range(48, 58)
- ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- SCALE = (60 + x for x in range(len(ALPHABET)))
-
- FLOWER = """
- .-~~-.--. WELCOME TO
- : ) CHEEKYFSM
- .~ ~ -.\ /.- ~~ .
- > `. .' <
- ( .- -. )
- `- -.-~ `- -' ~-.- -'
- ( : ) _ _ .-:
- ~--. : .--~ .-~ .-~ }
- ~-.-^-.-~ \_ .~ .-~ .~
- \ \' \ '_ _ -~
- `.`. //
- . - ~ ~-.__`.`-.//
- .-~ . - ~ }~ ~ ~-.~-.
- .' .-~ .-~ :/~-.~-./:
- /_~_ _ . - ~ ~-.~-._
- ~-.<
- """
-
-
- class CheekyFSM(object):
- def __init__(self):
- self.lastkey = -1
- self.buffer = ""
- self.message = ""
- self.edges = {}
- self.durations = {}
- self.state = "A"
- self.history = " " * 30
- self.repeat = 0
- self.osc = udp_client.SimpleUDPClient("0.0.0.0", 5005)
- self.bpm = 120
- self.last_update = time.time()
- self.scale = dict(zip(ALPHABET, SCALE))
-
- def show_edges(self):
- """ Print the help menu """
- y, x = self.w.getmaxyx()
- self.w.addstr(10, 2, f"Transitions:", curses.A_DIM)
- for index, ((start, end), prob) in enumerate(self.edges.items()):
- self.w.addstr(11 + index, 2, f"{start} -> {end} : {prob}%")
-
- def show_flower(self):
- y, x = self.w.getmaxyx()
- y -= 20
- x -= 45
- for index, line in enumerate(FLOWER.split("\n")):
- self.w.addstr(y + index, x, line, curses.A_DIM)
-
- def show_durations(self):
- """ Print the help menu """
- y, x = self.w.getmaxyx()
- self.w.addstr(10, 30, f"Durations:", curses.A_DIM)
- for index, (key, dur) in enumerate(self.durations.items()):
- self.w.addstr(11 + index, 30, f"{key} : {dur}♪")
-
- def show_scale(self):
- """ Print the scale """
- y, x = self.w.getmaxyx()
- self.w.addstr(10, 30, f"Scale:", curses.A_DIM)
- for index, (key, dur) in enumerate(self.scale.items()):
- self.w.addstr(11 + index, 30, f"{key} : {dur}")
-
- def update(self):
- """ Update the FSM """
- self.repeat += 1
- if self.repeat < self.durations.get(self.state, 1):
- self.osc.send_message(self.get_note(), 0)
- self.history = self.history[1:] + "."
- return
- self.repeat = 0
-
- changed = False
- edges = list(self.edges.items())
- random.shuffle(edges)
- for ((start, end), prob) in edges:
- if start == self.state:
- val = random.randint(0, 99)
- self.message = f"{start}, {end}, {val}, {prob}"
- if val < prob:
- self.state = end
- changed = True
- break
-
- if changed is False:
- self.history = self.history[1:] + self.state.lower()
- else:
- self.osc.send_message(self.get_note(), 80)
- self.history = self.history[1:] + self.state
- self.last_update = time.time()
-
- def show_state(self):
- """ Show the state of the machine """
- self.w.addstr(2, 2, f"Tempo: {self.bpm} bpm", curses.A_DIM)
- self.w.addstr(4, 2, f"State: {self.state}")
- self.w.addstr(2, 30, f"{self.history}")
-
- offset = 5
- for ((start, end), prob) in self.edges.items():
- if start == self.state:
- self.w.addstr(offset, 2, f"-> {end} with {prob}%",
- curses.A_DIM)
- offset += 1
-
- def show_status(self):
- """ Print the status bar """
- y, x = self.w.getmaxyx()
- self.w.addstr(y - 2, 2, f"> {self.buffer}", curses.A_BOLD)
-
- def show_debug(self):
- """ Print the status bar """
- y, x = self.w.getmaxyx()
- self.w.addstr(y - 4, 2, f"{self.message}", curses.A_DIM)
-
- def quit(self):
- """ Quit the program """
- self.osc.send_message(self.get_note(), 0)
- curses.nocbreak()
- self.w.keypad(False)
- curses.echo()
- curses.endwin()
- sys.exit(0)
-
- def process_key(self):
- """ Process a key """
- key = self.w.getch()
-
- self.lastkey = key
- if key == ESCAPE:
- quit(self.w)
- elif key in LETTERS:
- self.buffer += chr(key).upper()
- elif key in NUMBERS:
- self.buffer += chr(key)
- elif key == 10:
- self.accept()
- elif key == 263 and len(self.buffer) > 0:
- self.buffer = self.buffer[:-1]
- elif key == curses.KEY_UP:
- self.set_bpm(self.bpm + 10)
- elif key == curses.KEY_DOWN:
- self.set_bpm(self.bpm - 10)
-
- def set_bpm(self, bpm):
- """ Set the BPM """
- self.bpm = bpm
- self.w.timeout(int(1000 * 60 / self.bpm))
-
- def accept(self):
- """ Accept the string """
- # Match on edge
- match = re.match(r"([A-Z])([A-Z])(\d+)", self.buffer)
- if match:
- groups = match.groups()
- start = groups[0]
- end = groups[1]
- prob = int(groups[2])
- prob = max(0, min(100, prob))
- self.modify(start, end, prob)
- self.buffer = ""
- return
-
- # Match on duration / scale
- match = re.match(r"([A-Z])(\d+)", self.buffer)
- if match:
- groups = match.groups()
- key = groups[0]
- value = int(groups[1])
- if value > 0 and value < 1000:
- self.scale[key] = value
- self.buffer = ""
- return
-
- # Match on switch
- match = re.match(r"([A-Z])", self.buffer)
- if match:
- groups = match.groups()
- self.state = groups[0]
- self.history = self.history[1:] + self.state
- self.osc.send_message(self.get_note(), 80)
- self.buffer = ""
- return
-
- self.buffer = ""
-
- def get_note(self):
- """ Get the note """
- note = self.scale.get(self.state, 60)
- return f"i/vkb_midi/1/note/{note}"
-
- def modify(self, start, end, prob):
- """ Make a modification """
- key = (start, end)
- self.edges[key] = prob
- if prob == 0 and key in self.edges:
- del self.edges[key]
-
- def loop(self, w):
- """ Run the loop """
- self.w = w
- curses.curs_set(0) # invisible
- w.timeout(int(1000 * 60 / self.bpm))
- while True:
- self.process_key()
- w.clear()
- self.show_edges()
- self.show_status()
- self.show_debug()
- self.show_state()
- # self.show_durations()
- self.show_scale()
- self.show_flower()
- time.sleep(.01)
- self.osc.send_message(self.get_note(), 0)
- self.update()
- w.refresh()
- # time.sleep(max(0, expect - (time.time() - t)))
-
-
- if __name__ == "__main__":
- fsm = CheekyFSM()
- curses.wrapper(fsm.loop)
|