# Requirements: pyserial, hypy_utils, pygame import ctypes import math import os import threading from typing import NamedTuple, Literal import pygame import serial from hypy_utils.logging_utils import setup_logger log = setup_logger() class TouchArea(NamedTuple): category: Literal['A', 'B', 'C', 'D', 'E'] index: int def __str__(self): return self.category + str(self.index) touch_areas = ( [TouchArea('A', i + 1) for i in range(8)] + [TouchArea('B', i + 1) for i in range(8)] + [TouchArea('C', i + 1) for i in range(2)] + [TouchArea('D', i + 1) for i in range(8)] + [TouchArea('E', i + 1) for i in range(8)] ) def convert(d: str) -> list[TouchArea]: """ Convert maimai touch data string into touch areas activated. The maimai touch data is a letter representation of bitwise data. When nothing is touched, d = "@@@@@@@" When A1 is touched, d = "A@@@@@@" When A2 is touched, d = "B@@@@@@" When A3 is touched, d = "D@@@@@@" When both A1 and A2 are touched, d = "C@@@@@@" (A + B = C) When both A1 and A3 are touched, d = "E@@@@@@" (A + D = E) When all three are touched, d = "G@@@@@@" (A + B + D = G) Raw letter orders are A (1) B (10) D (100) H (1000) P (10000) :param d: Maimai touch data (single frame) :return: List of touch areas activated """ # Loop through each digit for ci, c in enumerate(d): if c == "@": continue # Convert it to ordinal number (A = 1) # The binary of oi will show 5 bits, each representing if the touch area is activated. oi = ord(c) - ord('A') + 1 # Find bits activated for i in range(5): if oi & (1 << i): yield touch_areas[ci * 5 + i] def serial_listen(callback: callable, port: str = 'COM3'): """ Listen for serial data from mai touch device. Official port: COM3 Baud rate: 9600 Frame format: (@@@@@@@) :param callback: Callback function when touch data is received :param port: Serial port to listen to """ # Open serial ser = serial.Serial(port, 9600) # Align frame while ser.read(1).decode('utf-8') != ')': pass # Loop forever frame = '' while True: # Read a frame frame += ser.read(9).decode('utf-8') log.debug(f"Received frame: {frame}") assert frame[0] == '(' assert frame[-1] == ')' assert len(frame) == 9 # Parse ta = list(convert(frame[1:-1])) frame = '' # Callback callback(ta) def serial_listen_btn(callback: callable, port: str = 'COM25'): """ Listen for serial data from mai_pico io4 buttons. Frame format: BTN 000000000000;\n :param callback: Callback function when button state changes :param port: Serial port to listen to """ # Open serial ser = serial.Serial(port, 9600) # Loop forever while True: ser.read_until(b'BTN ') frame = ser.read_until(b';').decode('utf-8') log.debug(f"Received frame: {frame}") # Callback callback(frame.strip(';')) def test_callback(ta: list[TouchArea]): log.info(f"Received touch areas: {' '.join(map(str, ta))}") def test_callback_btn(btn: str): log.info(f"Received button state: {btn}") # Configuration: VK codes for DFJK TAIKO_KEYS = [0x44, 0x46, 0x4A, 0x4B] SCAN_CODES = [ctypes.windll.user32.MapVirtualKeyA(k, 0) for k in TAIKO_KEYS] # State and macros TAIKO_STATES = [False] * len(TAIKO_KEYS) TAIKO_LEFT_KA = 0 TAIKO_LEFT_DON = 1 TAIKO_RIGHT_DON = 2 TAIKO_RIGHT_KA = 3 def taiko_callback(ta: list[TouchArea]): """ Convert mai touch to taiko keyboard input D1, D5, E1, E5 are ignored (they're located in the center A1-4, B1-4, D2-4, E2-4, C1 are right Don Others are left Don """ states = [False] * len(TAIKO_KEYS) for t in ta: if str(t) in ('D1', 'D5', 'E1', 'E5'): continue if (t.category in 'ABDE' and t.index in (1, 2, 3, 4)) or \ (t.category == 'C' and t.index == 1): states[TAIKO_RIGHT_DON] = True else: states[TAIKO_LEFT_DON] = True taiko_after_update(states) def taiko_after_update(states: list[bool], is_btn: bool = False): # Check state changes for i, (old, new) in enumerate(zip(TAIKO_STATES, states)): if ((is_btn and i in (TAIKO_LEFT_DON, TAIKO_RIGHT_DON)) or (not is_btn and i in (TAIKO_LEFT_KA, TAIKO_RIGHT_KA))): continue if old == new: continue TAIKO_STATES[i] = new # Send key up/down ctypes.windll.user32.keybd_event(TAIKO_KEYS[i], SCAN_CODES[i], 0 if new else 2, 0) def taiko_callback_btn(btn: str): """ Convert mai_pico io4 buttons to taiko keyboard input Button index 0-3 are right Ka, 4-7 are left Ka """ print(btn) states = [False] * len(TAIKO_KEYS) states[TAIKO_RIGHT_KA] = '1' in btn[0:4] states[TAIKO_LEFT_KA] = '1' in btn[4:8] taiko_after_update(states, True) def start_serial_threads(): thread1 = threading.Thread(target=serial_listen, args=(taiko_callback, 'COM3')) thread2 = threading.Thread(target=serial_listen_btn, args=(taiko_callback_btn, 'COM25')) thread1.start() thread2.start() if __name__ == '__main__': # Default window location at bottom os.environ['SDL_VIDEO_WINDOW_POS'] = f'0,{1920-1080}' # Start pygame window pygame.init() W = 1080 CW = 0.8 pygame.display.set_mode((W, W)) pygame.display.set_caption("MaiTouch") # Start serial threads start_serial_threads() COLOR_DON_OFF = '#fc9f9f' COLOR_DON_ON = '#ff4242' COLOR_KA_OFF = '#9fe3fc' COLOR_KA_ON = '#0f68f7' DELTA_DEG = 0.1 def color(idx): if idx in (TAIKO_LEFT_KA, TAIKO_RIGHT_KA): return COLOR_KA_ON if TAIKO_STATES[idx] else COLOR_KA_OFF return COLOR_DON_ON if TAIKO_STATES[idx] else COLOR_DON_OFF # Game loop try: while True: for event in pygame.event.get(): if event.type == pygame.QUIT: os._exit(0) # White background pygame.display.get_surface().fill((255, 255, 255)) # Taiko Don (two red half-circles in the center) mar = (W * (1 - CW)) / 2 pygame.draw.arc(pygame.display.get_surface(), color(TAIKO_LEFT_DON), (mar, mar, W * CW, W * CW), math.pi / 2, - math.pi / 2, 5000) pygame.draw.arc(pygame.display.get_surface(), color(TAIKO_RIGHT_DON), (mar, mar, W * CW, W * CW), - math.pi / 2, math.pi / 2, 5000) # Taiko Ka (two blue arches (empty half-circles) on the sides) pygame.draw.arc(pygame.display.get_surface(), color(TAIKO_LEFT_KA), (0, 0, W, W), math.pi / 2, - math.pi / 2, 80) pygame.draw.arc(pygame.display.get_surface(), color(TAIKO_RIGHT_KA), (0, 0, W, W), - math.pi / 2, math.pi / 2, 80) # Thin rectangle dividing the middle rw = 20 pygame.draw.rect(pygame.display.get_surface(), '#FFFFFF', (W / 2 - rw / 2, 0, rw, W)) pygame.display.flip() except KeyboardInterrupt: os._exit(0)