TUI OpenSoundControl finite state machine in Python
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

236 lignes
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)