#!/usr/bin/env python2.4 """ turcanator: a (somewhat primitive) midi piano tutor """ import sys, pygame import clockwork import readmidi import keyboard from keyboard import PRESS, HOLD, UNPRESSED import time from anymidi import device #def drawKey(surf, keyNum, shade): # pygame.draw.rect(surf, [200,150,100] #def drawGenericPiano(surf, x, y, keyw, keyh, keycount, shades={}): # for keyNum in range(keycount): # drawKey(surf, keyNum, shades.get(keyNum) or ) class Rectangle(object): def __init__(self, w,h, color, highlight, x=0,y=0): self.rect = (x,y,w,h) self.color = color self.border = (50,50,50) self.highlight = highlight def translate(self, dx,dy): x,y,w,h = self.rect return self.__class__(w, h, self.color, self.highlight, x+dx, y+dy) def draw(self, surface): x,y,w,h = self.rect # draw black outline: pygame.draw.rect(surface, self.border, pygame.Rect(*self.rect), 1) # draw square itself: pygame.draw.rect(surface, self.color, pygame.Rect(x+1,y+1,w-1,h-1)) if self.highlight: if self.highlight == blue2: # kludge for continued notes... pygame.draw.rect(surface, blue, pygame.Rect(x+2,y-3,w-3,h+2)) else: pygame.draw.rect(surface, self.highlight, pygame.Rect(x+2,y+2,w-3,h-3)) class dim: white = [150,150,150] black = [100,100,100] class bright: white = [255,255,255] black = [15,15,15] def keycolor(palette, keyNum): if keyNum % 12 in (1, 3, 6, 8, 10): return palette.black else: return palette.white keyh=7 def drawPiano(x, y, palette, colors, keyw=10, keyh=keyh): for k in range(60): (Rectangle(keyw,keyh,keycolor(palette, k),colors.get(k)) .translate(x+keyw*k, y) .draw(screen)) def drawScore(score, x): for i, row in enumerate(score): drawPiano(x, yPos(i), dim, colors(row)) red = (255,0,0) gold = (200,100,10) green = (50, 250, 25) blue = (20, 50, 255) blue2= (60, 200, 255) brackcolor = (250,150,50) colormap = { readmidi.PRESS : blue, readmidi.HOLD : blue2, } def colors(keys): res = {} for k,v in enumerate(keys): res[k] = colormap.get(v) return res def comparison(goals, actuals): res = {} for k, (goal, actual) in enumerate(zip(goals, actuals)): res[k] = compareOne(goal, actual) return res def compareOne(goal, actual): if goal in (PRESS, HOLD): if actual in (PRESS, HOLD): return green else: return colormap[goal] else: if actual in (PRESS, HOLD): return red else: return None def yPos(y): offset = y / 8 # add an extra line every 8 boxes (2:4 time sig) offset += 2 * (y / 16) # +2 every 16 (remeber 2/16 != 1/8 for ints!!) return offset + 10 + y * keyh WINH=700 def drawbracket(screen, top, bot): pygame.draw.rect(screen, (0,0,0), pygame.Rect(5, 0, 5, WINH)) pygame.draw.rect(screen, brackcolor, pygame.Rect(5, yPos(top), 5, yPos(bot-top))) def play_midi(score, start, stop): print "playing midi!" NoteOn = 0x90 NoteOff = 0x80 prev = [0] * keyboard.numkeys # blank piano for state in score[start:stop+1]: # @TODO: may need to tweak offsets for (e, (was, now)) in enumerate(zip(prev, state)): i = e + keyboard.leftmost_key if now == PRESS: print "note 0x", hex(i), "=", i, "on" device.send(NoteOff, i, 0) device.send(NoteOn, i, 32) elif (now == UNPRESSED) and (was != UNPRESSED): print "note 0x", hex(i), "=", i, "off" device.send(NoteOff, i, 0) prev = state yield clockwork.sleep(0.25) # now be quiet, even if selection cuts off mid-note: for i in range(keyboard.numkeys): device.send(NoteOff, i+keyboard.leftmost_key, 0) if __name__ == '__main__': if len(sys.argv) > 1: midifile = sys.argv[1] else: midifile = 'rondo-alla-turca-a.mid' worker = clockwork.Worker() pygame.init() screen = pygame.display.set_mode([620, WINH]) pygame.display.set_caption("turcanator 0.1") screen.fill([0,0,0]) score = readmidi.buildScore(midifile) drawScore(score, 10) pygame.display.flip() # ----- liveKeyboard = keyboard.Keyboard() device.pyCallback = liveKeyboard def clearbright(line): drawPiano(10, yPos(line), dim, colors(score[pos])) loop_top = 0 loop_bot = len(score)-1 drawbracket(screen, loop_top, loop_bot) def loopback(pos): if pos > loop_bot: return loop_top return pos pos = 0 while True: worker.tick() #pygame.time.delay(1) event = pygame.event.poll() if event.type == pygame.QUIT: break elif event.type == pygame.KEYDOWN: # cursor navigation clearbright(pos) if event.key == pygame.K_ESCAPE: break elif event.key == pygame.K_DOWN: pos += 1 elif event.key == pygame.K_UP: pos -= 1 elif event.key == pygame.K_PAGEDOWN: pos += 8 elif event.key == pygame.K_PAGEUP: pos -= 8 # loopback support: elif event.key == pygame.K_LEFTBRACKET: loop_top = pos drawbracket(screen, loop_top, loop_bot) elif event.key == pygame.K_RIGHTBRACKET: loop_bot = pos drawbracket(screen, loop_top, loop_bot) elif event.key == pygame.K_SPACE: pos = loop_top # player support: elif event.key == pygame.K_p: worker.assign(play_midi(score, loop_top, loop_bot)) # bounds checking: if pos >= len(score): pos = len(score)-1 if pos < 0 : pos = 0 snap = liveKeyboard.snapshot() if snap==[n*n for n in score[pos]]: clearbright(pos) pos = loopback(pos + 1) else: pass drawPiano(10, yPos(pos), bright, comparison(score[pos],snap)) pygame.display.flip() pygame.display.quit()