TUI OpenSoundControl finite state machine in Python
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

236 lines
7.0KB

  1. from pythonosc import udp_client
  2. import curses
  3. import random
  4. import sys
  5. import re
  6. import time
  7. ESCAPE = 27
  8. LETTERS = range(97, 123)
  9. NUMBERS = range(48, 58)
  10. ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  11. SCALE = (60 + x for x in range(len(ALPHABET)))
  12. FLOWER = """
  13. .-~~-.--. WELCOME TO
  14. : ) CHEEKYFSM
  15. .~ ~ -.\ /.- ~~ .
  16. > `. .' <
  17. ( .- -. )
  18. `- -.-~ `- -' ~-.- -'
  19. ( : ) _ _ .-:
  20. ~--. : .--~ .-~ .-~ }
  21. ~-.-^-.-~ \_ .~ .-~ .~
  22. \ \' \ '_ _ -~
  23. `.`. //
  24. . - ~ ~-.__`.`-.//
  25. .-~ . - ~ }~ ~ ~-.~-.
  26. .' .-~ .-~ :/~-.~-./:
  27. /_~_ _ . - ~ ~-.~-._
  28. ~-.<
  29. """
  30. class CheekyFSM(object):
  31. def __init__(self):
  32. self.lastkey = -1
  33. self.buffer = ""
  34. self.message = ""
  35. self.edges = {}
  36. self.durations = {}
  37. self.state = "A"
  38. self.history = " " * 30
  39. self.repeat = 0
  40. self.osc = udp_client.SimpleUDPClient("0.0.0.0", 5005)
  41. self.bpm = 120
  42. self.last_update = time.time()
  43. self.scale = dict(zip(ALPHABET, SCALE))
  44. def show_edges(self):
  45. """ Print the help menu """
  46. y, x = self.w.getmaxyx()
  47. self.w.addstr(10, 2, f"Transitions:", curses.A_DIM)
  48. for index, ((start, end), prob) in enumerate(self.edges.items()):
  49. self.w.addstr(11 + index, 2, f"{start} -> {end} : {prob}%")
  50. def show_flower(self):
  51. y, x = self.w.getmaxyx()
  52. y -= 20
  53. x -= 45
  54. for index, line in enumerate(FLOWER.split("\n")):
  55. self.w.addstr(y + index, x, line, curses.A_DIM)
  56. def show_durations(self):
  57. """ Print the help menu """
  58. y, x = self.w.getmaxyx()
  59. self.w.addstr(10, 30, f"Durations:", curses.A_DIM)
  60. for index, (key, dur) in enumerate(self.durations.items()):
  61. self.w.addstr(11 + index, 30, f"{key} : {dur}♪")
  62. def show_scale(self):
  63. """ Print the scale """
  64. y, x = self.w.getmaxyx()
  65. self.w.addstr(10, 30, f"Scale:", curses.A_DIM)
  66. for index, (key, dur) in enumerate(self.scale.items()):
  67. self.w.addstr(11 + index, 30, f"{key} : {dur}")
  68. def update(self):
  69. """ Update the FSM """
  70. self.repeat += 1
  71. if self.repeat < self.durations.get(self.state, 1):
  72. self.osc.send_message(self.get_note(), 0)
  73. self.history = self.history[1:] + "."
  74. return
  75. self.repeat = 0
  76. changed = False
  77. edges = list(self.edges.items())
  78. random.shuffle(edges)
  79. for ((start, end), prob) in edges:
  80. if start == self.state:
  81. val = random.randint(0, 99)
  82. self.message = f"{start}, {end}, {val}, {prob}"
  83. if val < prob:
  84. self.state = end
  85. changed = True
  86. break
  87. if changed is False:
  88. self.history = self.history[1:] + self.state.lower()
  89. else:
  90. self.osc.send_message(self.get_note(), 80)
  91. self.history = self.history[1:] + self.state
  92. self.last_update = time.time()
  93. def show_state(self):
  94. """ Show the state of the machine """
  95. self.w.addstr(2, 2, f"Tempo: {self.bpm} bpm", curses.A_DIM)
  96. self.w.addstr(4, 2, f"State: {self.state}")
  97. self.w.addstr(2, 30, f"{self.history}")
  98. offset = 5
  99. for ((start, end), prob) in self.edges.items():
  100. if start == self.state:
  101. self.w.addstr(offset, 2, f"-> {end} with {prob}%",
  102. curses.A_DIM)
  103. offset += 1
  104. def show_status(self):
  105. """ Print the status bar """
  106. y, x = self.w.getmaxyx()
  107. self.w.addstr(y - 2, 2, f"> {self.buffer}", curses.A_BOLD)
  108. def show_debug(self):
  109. """ Print the status bar """
  110. y, x = self.w.getmaxyx()
  111. self.w.addstr(y - 4, 2, f"{self.message}", curses.A_DIM)
  112. def quit(self):
  113. """ Quit the program """
  114. self.osc.send_message(self.get_note(), 0)
  115. curses.nocbreak()
  116. self.w.keypad(False)
  117. curses.echo()
  118. curses.endwin()
  119. sys.exit(0)
  120. def process_key(self):
  121. """ Process a key """
  122. key = self.w.getch()
  123. self.lastkey = key
  124. if key == ESCAPE:
  125. quit(self.w)
  126. elif key in LETTERS:
  127. self.buffer += chr(key).upper()
  128. elif key in NUMBERS:
  129. self.buffer += chr(key)
  130. elif key == 10:
  131. self.accept()
  132. elif key == 263 and len(self.buffer) > 0:
  133. self.buffer = self.buffer[:-1]
  134. elif key == curses.KEY_UP:
  135. self.set_bpm(self.bpm + 10)
  136. elif key == curses.KEY_DOWN:
  137. self.set_bpm(self.bpm - 10)
  138. def set_bpm(self, bpm):
  139. """ Set the BPM """
  140. self.bpm = bpm
  141. self.w.timeout(int(1000 * 60 / self.bpm))
  142. def accept(self):
  143. """ Accept the string """
  144. # Match on edge
  145. match = re.match(r"([A-Z])([A-Z])(\d+)", self.buffer)
  146. if match:
  147. groups = match.groups()
  148. start = groups[0]
  149. end = groups[1]
  150. prob = int(groups[2])
  151. prob = max(0, min(100, prob))
  152. self.modify(start, end, prob)
  153. self.buffer = ""
  154. return
  155. # Match on duration / scale
  156. match = re.match(r"([A-Z])(\d+)", self.buffer)
  157. if match:
  158. groups = match.groups()
  159. key = groups[0]
  160. value = int(groups[1])
  161. if value > 0 and value < 1000:
  162. self.scale[key] = value
  163. self.buffer = ""
  164. return
  165. # Match on switch
  166. match = re.match(r"([A-Z])", self.buffer)
  167. if match:
  168. groups = match.groups()
  169. self.state = groups[0]
  170. self.history = self.history[1:] + self.state
  171. self.osc.send_message(self.get_note(), 80)
  172. self.buffer = ""
  173. return
  174. self.buffer = ""
  175. def get_note(self):
  176. """ Get the note """
  177. note = self.scale.get(self.state, 60)
  178. return f"i/vkb_midi/1/note/{note}"
  179. def modify(self, start, end, prob):
  180. """ Make a modification """
  181. key = (start, end)
  182. self.edges[key] = prob
  183. if prob == 0 and key in self.edges:
  184. del self.edges[key]
  185. def loop(self, w):
  186. """ Run the loop """
  187. self.w = w
  188. curses.curs_set(0) # invisible
  189. w.timeout(int(1000 * 60 / self.bpm))
  190. while True:
  191. self.process_key()
  192. w.clear()
  193. self.show_edges()
  194. self.show_status()
  195. self.show_debug()
  196. self.show_state()
  197. # self.show_durations()
  198. self.show_scale()
  199. self.show_flower()
  200. time.sleep(.01)
  201. self.osc.send_message(self.get_note(), 0)
  202. self.update()
  203. w.refresh()
  204. # time.sleep(max(0, expect - (time.time() - t)))
  205. if __name__ == "__main__":
  206. fsm = CheekyFSM()
  207. curses.wrapper(fsm.loop)