147 lines
4.0 KiB
Python
Executable File
147 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import sys
|
|
import os
|
|
from typing import List
|
|
|
|
import pygame as pg
|
|
from pygame.constants import KEYDOWN
|
|
import pygame.midi
|
|
from mido import MidiFile
|
|
|
|
# Status definitions
|
|
TOUCH_DOWN = 144
|
|
TOUCH_UP = 128
|
|
|
|
|
|
class Key:
|
|
def __init__(self, key: int, start: int, duration: int):
|
|
self.key = key
|
|
self.start = start
|
|
self.duration = duration
|
|
|
|
def __str__(self):
|
|
return f"{self.key} ({self.start} - {self.duration})"
|
|
|
|
|
|
def read_midi(file):
|
|
notes = []
|
|
notes_on = {}
|
|
s = 0
|
|
for msg in MidiFile(file):
|
|
d = msg.dict()
|
|
s += d['time'] * 1000
|
|
if d["type"] == "note_on":
|
|
notes_on[d["note"]] = s
|
|
if d["type"] == "note_off":
|
|
duration = s - notes_on[d["note"]]
|
|
notes_on[d["note"]] = s
|
|
notes.append(Key(d["note"], s, duration))
|
|
return notes
|
|
|
|
|
|
keys_to_play = read_midi(sys.argv[1])
|
|
for i in map(lambda x: str(x), keys_to_play):
|
|
print(str(i))
|
|
|
|
# List of keys currently holded. Format: (key, timestamp)
|
|
keys_down = []
|
|
|
|
points = 0
|
|
|
|
|
|
def print_device_info():
|
|
pygame.midi.init()
|
|
_print_device_info()
|
|
pygame.midi.quit()
|
|
|
|
|
|
def _print_device_info():
|
|
for i in range(pygame.midi.get_count()):
|
|
r = pygame.midi.get_device_info(i)
|
|
(interf, name, input, output, opened) = r
|
|
|
|
in_out = ""
|
|
if input:
|
|
in_out = "(input)"
|
|
if output:
|
|
in_out = "(output)"
|
|
|
|
print(
|
|
"%2i: interface :%s:, name :%s:, opened :%s: %s"
|
|
% (i, interf, name, opened, in_out)
|
|
)
|
|
|
|
def poll(midi):
|
|
if midi.poll():
|
|
[((status, key, intensity, data3), timestamp)] = midi.read(1)
|
|
# For status, see STATUS DEFINITIONS up there (either TOUCH_DOWN, TOUCH_UP or others for pedals)
|
|
# The key is between 21 and 108, C5 is 60
|
|
# The itensity is how strong the key got struck (between 1 and 130ish)
|
|
# data3 seems to always be 0
|
|
# timestamp seems to be a unix timestamp since the midi has been oppened.
|
|
|
|
# Sometimes, status is always TOUCH_DOWN so if the key is already down, we consider it the same as a key up
|
|
is_down = any(x[0] == key for x in keys_down)
|
|
if status == TOUCH_DOWN and not is_down:
|
|
keys_down.append((key, timestamp))
|
|
# print(f"Midi: {status} - {key} - {intensity} - {data3} at {timestamp}")
|
|
elif status == TOUCH_UP or is_down:
|
|
down_since = next(since for (h_key, since) in keys_down if h_key == key)
|
|
keys_down.remove((key, down_since))
|
|
return Key(key, down_since, (timestamp - down_since))
|
|
|
|
def is_timing_close(key, i):
|
|
return abs(i.start - key.start) < 500
|
|
|
|
def run(midi):
|
|
global points
|
|
clock_now = 0
|
|
while sorted(keys_to_play, key=lambda x: x.start)[-1].start > clock_now:
|
|
key = poll(midi)
|
|
if key is None:
|
|
continue
|
|
clock_now = key.start
|
|
|
|
to_play = next((i for i in keys_to_play if i.key == key.key and is_timing_close(key, i)), None)
|
|
if to_play == None:
|
|
points -= 50
|
|
print(f"Invalid key.")
|
|
else:
|
|
tempo_percent = abs((key.duration / to_play.duration) - 1)
|
|
points += tempo_percent * 50
|
|
if tempo_percent < .3 :
|
|
print("Too short" if key.duration < to_play.duration else "Too long")
|
|
elif tempo_percent < .5:
|
|
print(f"GREAT.")
|
|
else:
|
|
print(f"EXCELLENT.")
|
|
points -= len(keys_to_play) * 20
|
|
|
|
|
|
def input_main(device_id=None):
|
|
pg.init()
|
|
pygame.midi.init()
|
|
|
|
_print_device_info()
|
|
|
|
if device_id is None:
|
|
input_id = pygame.midi.get_default_input_id()
|
|
else:
|
|
input_id = device_id
|
|
|
|
print("using input_id :%s:" % input_id)
|
|
i = pygame.midi.Input(input_id)
|
|
|
|
pg.display.set_mode((1, 1))
|
|
try:
|
|
run(i)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
print(f"You got: {int(points)}pts")
|
|
pygame.midi.quit()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
exit(input_main(int(sys.argv[2]) if len(sys.argv) == 3 else None))
|