diff --git a/cheekyfsm.py b/cheekyfsm.py index f7d26fc..d841d79 100644 --- a/cheekyfsm.py +++ b/cheekyfsm.py @@ -1,11 +1,34 @@ +from pythonosc import udp_client import curses import random import sys import re +import time ESCAPE = 27 -LETTERS = range(97, 122) -NUMBERS = range(48, 57) +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): @@ -14,37 +37,82 @@ class CheekyFSM(object): self.buffer = "" self.message = "" self.edges = {} + self.durations = {} self.state = "A" - self.history = " "*30 + 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(10 + index, 2, f"{start} -> {end} : {prob}%") + 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: - if random.randint(0, 100) < prob: + val = random.randint(0, 99) + self.message = f"{start}, {end}, {val}, {prob}" + if val < prob: self.state = end + changed = True + break - self.history = self.history[1:] + self.state + 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: 120bpm") + 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(5, 2, f"History: {self.history}") + self.w.addstr(2, 30, f"{self.history}") - offset = 6 + offset = 5 for ((start, end), prob) in self.edges.items(): if start == self.state: - self.w.addstr(offset, 2, - f"Going to {end} with probability {prob}%") + self.w.addstr(offset, 2, f"-> {end} with {prob}%", + curses.A_DIM) offset += 1 def show_status(self): @@ -52,8 +120,14 @@ class CheekyFSM(object): 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() @@ -63,6 +137,7 @@ class CheekyFSM(object): def process_key(self): """ Process a key """ key = self.w.getch() + self.lastkey = key if key == ESCAPE: quit(self.w) @@ -74,42 +149,85 @@ class CheekyFSM(object): 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 not match: - self.message = " Invalid command :(" - else: + 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 - self.message = f" {start} -> {end} : {prob}%" if prob == 0 and key in self.edges: - self.message = f" {start} -> {end} : deleted" del self.edges[key] def loop(self, w): """ Run the loop """ self.w = w curses.curs_set(0) # invisible - w.timeout(100) + 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() - self.process_key() + # time.sleep(max(0, expect - (time.time() - t))) if __name__ == "__main__":