Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions pianoputer/keyboards/custom_qwerty.kb
Original file line number Diff line number Diff line change
@@ -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
\
27 changes: 13 additions & 14 deletions pianoputer/keyboards/qwerty_piano.txt
Original file line number Diff line number Diff line change
@@ -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
\
43 changes: 43 additions & 0 deletions pianoputer/keyboards/qwerty_piano.txt.BAK
Original file line number Diff line number Diff line change
@@ -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
\
5 changes: 4 additions & 1 deletion pianoputer/make_kb_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
130 changes: 122 additions & 8 deletions pianoputer/pianoputer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import numpy
import pygame
import soundfile
import mido

ANCHOR_INDICATOR = " anchor"
ANCHOR_NOTE_REGEX = re.compile(r"\s[abcdefg]$")
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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],
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down