diff --git a/README.md b/README.md index d2d08ce..9006375 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ All white and black keys are transposed up and down from the anchor cyan key. ![qwerty keyboard layout, c4 is cyan](https://raw.githubusercontent.com/Zulko/pianoputer/master/pianoputer/keyboards/qwerty_piano.jpg "qwerty keyboard layout, c4 is cyan") +## Midi + +You can use pianoputer as a midi keybaord by using the `--midi` flag. By default, the `--midi` flag creates a virtual midi output port called "pianoputer", but you can also specify a real port, +such as `--midi COM1` or `--midi /dev/serial0`. + ## Changing the sound file You can provide your own sound file with diff --git a/pianoputer/keyboards/custom_qwerty.kb b/pianoputer/keyboards/custom_qwerty.kb new file mode 100644 index 0000000..48b1603 --- /dev/null +++ b/pianoputer/keyboards/custom_qwerty.kb @@ -0,0 +1,42 @@ +caps lock +left shift +a +z +s +x +tab +1 +q +2 +w +e +4 +r +5 +t +6 +y +v c +g +b +h +n +m +k +, +l +. +; +/ +right shift +8 +i +9 +o +p +- +[ += +] +backspace +\ diff --git a/pianoputer/keyboards/qwerty_piano.txt b/pianoputer/keyboards/qwerty_piano.txt index e3d57df..136af58 100644 --- a/pianoputer/keyboards/qwerty_piano.txt +++ b/pianoputer/keyboards/qwerty_piano.txt @@ -1,43 +1,42 @@ -` +caps lock +left shift +a +z +s +x tab 1 q +2 w -3 e 4 r 5 t -left shift -a -z -s -x -c -f +6 anchor +y v g b h n -m c +m k , l . +; / -' right shift -return -u 8 i +9 o -0 p - [ += ] backspace \ diff --git a/pianoputer/keyboards/qwerty_piano.txt.BAK b/pianoputer/keyboards/qwerty_piano.txt.BAK new file mode 100644 index 0000000..e3d57df --- /dev/null +++ b/pianoputer/keyboards/qwerty_piano.txt.BAK @@ -0,0 +1,43 @@ +` +tab +1 +q +w +3 +e +4 +r +5 +t +left shift +a +z +s +x +c +f +v +g +b +h +n +m c +k +, +l +. +/ +' +right shift +return +u +8 +i +o +0 +p +- +[ +] +backspace +\ diff --git a/pianoputer/make_kb_file.py b/pianoputer/make_kb_file.py index 4e76552..053f068 100755 --- a/pianoputer/make_kb_file.py +++ b/pianoputer/make_kb_file.py @@ -7,9 +7,12 @@ pg.init() screen = pg.display.set_mode((200, 200)) print("Press the keys in the right order. Press Escape to finish.") + while True: + event = pg.event.wait() - if event.type is pg.KEYDOWN: + + if event.type == pg.KEYDOWN: if event.key == pg.K_ESCAPE: break else: diff --git a/pianoputer/pianoputer.py b/pianoputer/pianoputer.py index b89c0b2..e6e4d8e 100644 --- a/pianoputer/pianoputer.py +++ b/pianoputer/pianoputer.py @@ -16,6 +16,7 @@ import numpy import pygame import soundfile +import mido ANCHOR_INDICATOR = " anchor" ANCHOR_NOTE_REGEX = re.compile(r"\s[abcdefg]$") @@ -33,6 +34,12 @@ CURRENT_WORKING_DIR = Path(__file__).parent.absolute() ALLOWED_EVENTS = {pygame.KEYDOWN, pygame.KEYUP, pygame.QUIT} +# declare globals +midi = False +midoutB = None +midoutT = None +keyboard_state_info = None +screen = None def get_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=DESCRIPTION) @@ -61,6 +68,14 @@ def get_parser() -> argparse.ArgumentParser: action="store_true", help="deletes stored transposed audio files and recalculates them", ) + parser.add_argument( + "--midi", "-m", + type=str, + help="enable midi, and provide midi port (optional)", + nargs='?', + default=None, + const="pianoputer" + ) parser.add_argument("--verbose", "-v", action="store_true", help="verbose mode") return parser @@ -99,10 +114,10 @@ def get_or_create_key_sounds( ) ) if channels == 1: - sound = librosa.effects.pitch_shift(y, sr, n_steps=tone) + sound = librosa.effects.pitch_shift(y=y, sr=sr, n_steps=tone) else: new_channels = [ - librosa.effects.pitch_shift(y[i], sr, n_steps=tone) + librosa.effects.pitch_shift(y=y[i], sr=sr, n_steps=tone) for i in range(channels) ] sound = numpy.ascontiguousarray(numpy.vstack(new_channels).T) @@ -168,7 +183,7 @@ def get_keyboard_info(keyboard_file: str): key_color = (65, 65, 65, 255) key_txt_color = (0, 0, 0, 255) for index, key in enumerate(keys): - if index == anchor_index: + if index == anchor_index and False: #ignore the anchor color_to_key[CYAN].append(key) continue if black_key_indices: @@ -262,6 +277,15 @@ def configure_pygame_audio_and_set_ui( screen_width = keyboard.rect.width screen_height = keyboard.rect.height + global screen, keyboard_state_info + + keyboard_state_info = ( + keyboard_arg, + color_to_key, + key_color, + key_txt_color + ) + screen = pygame.display.set_mode((screen_width, screen_height)) screen.fill(pygame.Color("black")) if keyboard: @@ -270,6 +294,64 @@ def configure_pygame_audio_and_set_ui( return screen, keyboard +def draw(keyboard_arg, color_to_key, key_color, key_txt_color): + + screen_width = 50 + screen_height = 50 + if "qwerty" in keyboard_arg: + layout_name = kl.LayoutName.QWERTY + elif "azerty" in keyboard_arg: + layout_name = kl.LayoutName.AZERTY_LAPTOP + else: + ValueError("keyboard must have qwerty or azerty in its name") + margin = 4 + key_size = 60 + overrides = {} + for color_value, keys in color_to_key.items(): + override_color = color = pygame.Color(color_value) + inverted_color = list(~override_color) + other_val = 255 + if ( + abs(color_value[0] - inverted_color[0]) > abs(color_value[0] - other_val) + ) or color_value == CYAN: + override_txt_color = pygame.Color(inverted_color) + else: + # biases grey override keys to use white as txt_color + override_txt_color = pygame.Color([other_val] * 3 + [255]) + override_key_info = kl.KeyInfo( + margin=margin, + color=override_color, + txt_color=override_txt_color, + txt_font=pygame.font.SysFont("Arial", key_size // 4), + txt_padding=(key_size // 10, key_size // 10), + ) + for key in keys: + overrides[key.value] = override_key_info + + key_txt_color = pygame.Color(key_txt_color) + keyboard_info = kl.KeyboardInfo(position=(0, 0), padding=2, color=key_txt_color) + key_info = kl.KeyInfo( + margin=margin, + color=pygame.Color(key_color), + txt_color=pygame.Color(key_txt_color), + txt_font=pygame.font.SysFont("Arial", key_size // 4), + txt_padding=(key_size // 6, key_size // 10), + ) + letter_key_size = (key_size, key_size) # width, height + keyboard = klp.KeyboardLayout( + layout_name, keyboard_info, letter_key_size, key_info, overrides + ) + screen_width = keyboard.rect.width + screen_height = keyboard.rect.height + + global screen + + #screen = pygame.display.set_mode((screen_width, screen_height)) + screen.fill(pygame.Color("black")) + keyboard.draw(screen) + pygame.display.update() + + def play_until_user_exits( keys: List[kl.Key], key_sounds: List[pygame.mixer.Sound], @@ -296,11 +378,35 @@ def play_until_user_exits( except KeyError: continue + keyN = keys.index(key) + int(os.environ.get("MIDI_KEY_OFFSET", 37 + 5)) + int(os.environ.get("MIDI_KEY_TRANSPOSE", 0)) + if event.type == pygame.KEYDOWN: - sound.stop() - sound.play(fade_ms=SOUND_FADE_MILLISECONDS) + + keyboard_state_info[1][CYAN].append(key) + + if not midi: + sound.stop() + sound.play(fade_ms=SOUND_FADE_MILLISECONDS) + else: + if keys.index(key) >= 18: + midoutB.send(mido.Message('note_on', note=keyN, velocity=100)) + else: + midoutT.send(mido.Message('note_on', note=keyN, velocity=100)) + elif event.type == pygame.KEYUP: - sound.fadeout(SOUND_FADE_MILLISECONDS) + + keyboard_state_info[1][CYAN].remove(key) + + if not midi: + sound.fadeout(SOUND_FADE_MILLISECONDS) + else: + if keys.index(key) >= 18: + midoutB.send(mido.Message('note_off', note=keyN, velocity=0)) + else: + midoutT.send(mido.Message('note_off', note=keyN, velocity=0)) + + draw(*keyboard_state_info) + pygame.display.flip() pygame.quit() print("Goodbye") @@ -333,12 +439,20 @@ def process_args(parser: argparse.ArgumentParser, args: Optional[List]) -> Tuple keyboard_path = args.keyboard if keyboard_path.startswith(KEYBOARD_ASSET_PREFIX): keyboard_path = os.path.join(CURRENT_WORKING_DIR, keyboard_path) - return wav_path, keyboard_path, args.clear_cache + return wav_path, keyboard_path, args.clear_cache, args.midi def play_pianoputer(args: Optional[List[str]] = None): + + global midi, midoutB, midoutT + parser = get_parser() - wav_path, keyboard_path, clear_cache = process_args(parser, args) + wav_path, keyboard_path, clear_cache, midi = process_args(parser, args) + + if midi: + midoutB = mido.open_output(midi, virtual=not (midi.startswith("COM") or midi.startswith("/dev/")), use_environ=True) + midoutT = mido.open_output(midi, virtual=not (midi.startswith("COM") or midi.startswith("/dev/")), use_environ=True) + audio_data, framerate_hz, channels = get_audio_data(wav_path) results = get_keyboard_info(keyboard_path) keys, tones, color_to_key, key_color, key_txt_color = results