import threading
try:
import urwid
except ImportError:
from .. import urwid
from .. import KEYS
from .. import CHROMATIC_SHARP
from .. import CHROMATIC_FLAT
from .. import INTERVALS
from ..questionbase import QUESTION_CLASSES
from ..note_and_pitch import get_pitch_by_number
from ..scale import ChromaticScale
from .. import D
KEY_PADS = {
'C#': 1,
'Db': 1,
'D#': 0,
'Eb': 0,
'F#': 1,
'Gb': 1,
'G#': 0,
'Ab': 0,
'A#': 0,
'Bb': 0
}
SPACE_CHAR = ' '
FILL_HEIGHT = 3
LOCK = threading.Lock()
[docs]def Pad(weight=1):
return ('weight',
weight,
urwid.BoxAdapter(urwid.SolidFill(SPACE_CHAR), height=FILL_HEIGHT))
[docs]def is_chromatic(key):
if len(key) == 2:
return True
return False
[docs]class Keyboard(urwid.Filler):
def __init__(self, scale, question_tonic_pitch, main_loop=None,
keyboard_index=None, *args, **kwargs):
self.main_loop = main_loop
self.scale = scale
self.key_index = {}
self.highlighted_keys = list()
tonic_pitch = scale[0]
tonic_str = scale[0].note
key_scale = [pitch for pitch in scale]
chromatic_keys = list()
diatonic_keys = list()
is_key_chromatic = is_chromatic(key=tonic_str)
# start (left) padding
first_pad = diatonic_keys if is_key_chromatic else chromatic_keys
if tonic_str == "E" or tonic_str == "B":
first_pad.append(Pad(weight=1.5))
else:
first_pad.append(Pad(weight=0.5))
first_chromatic = [pitch for pitch in key_scale
if len(pitch.note) == 2][0]
for index, pitch in enumerate(key_scale):
pitch_str = str(pitch)
note_str = pitch.note
_idx = abs(int(question_tonic_pitch) - int(pitch))
letter = keyboard_index[_idx]
#letter = keyboard_index[index]
bottom_text = letter
middle_text = INTERVALS[keyboard_index.index(letter)][1]
if is_chromatic(pitch.note):
if KEY_PADS[note_str] == 1 and (pitch is not first_chromatic):
chromatic_keys.append(Pad(weight=1))
chromatic_keys.append(KeyboardButton(pitch=pitch,
middle=middle_text,
bottom=bottom_text))
else:
diatonic_keys.append(KeyboardButton(pitch=pitch,
middle=middle_text,
bottom=bottom_text))
# end (right) padding:
if is_key_chromatic:
weight = 0.5
diatonic_keys.append(Pad(weight=weight))
else:
if KEY_PADS[first_chromatic.note]:
if tonic_str == "E" or tonic_str == "B":
weight = 0.5
else:
weight = (KEY_PADS[first_chromatic.note]/2) + 1
chromatic_keys.append(Pad(weight=weight))
if not KEY_PADS[first_chromatic.note]:
weight = 0.5
chromatic_keys.append(Pad(weight=weight))
self.key_index = {item.pitch_str: item
for item in chromatic_keys+diatonic_keys
if type(item).__name__ == 'KeyboardButton'}
chromatic = urwid.Columns(widget_list=chromatic_keys, dividechars=1)
diatonic = urwid.Columns(widget_list=diatonic_keys, dividechars=1)
keyboard = urwid.Pile([chromatic, diatonic])
box = urwid.LineBox(keyboard)
super(Keyboard, self).__init__(body=box, min_height=10,
*args, **kwargs)
[docs] def highlight_key(self, element=None):
with LOCK:
# for key in self.key_index.values():
for key in self.highlighted_keys:
# key.highlight(state=False)
self.key_index[key].highlight(state=False)
self.highlighted_keys.remove(key)
if type(element).__name__ == "Pitch":
pitch_str = str(element)
if pitch_str in self.key_index:
self.key_index[pitch_str].highlight(state=True)
self.highlighted_keys.append(pitch_str)
# D(self.highlighted_keys,2)
elif type(element).__name__ == "Chord":
for pitch in element:
chord_pitch_str = str(pitch)
if chord_pitch_str in self.key_index:
self.key_index[chord_pitch_str].highlight(state=True)
self.highlighted_keys.append(chord_pitch_str)
with LOCK:
self.main_loop.draw_screen()
[docs]class TextUserInterfaceWidget(urwid.Frame):
def __init__(self, *args, **kwargs):
self.footer_left = urwid.Text('r - Repeat question; q - Quit')
self.footer_right = urwid.Text('--', align='right')
header = urwid.AttrMap(urwid.Padding(urwid.Text('birdears ─ Functional'
' Ear Training')),
'header')
footer = urwid.AttrMap(urwid.Padding(urwid.Columns(widget_list=[
self.footer_left, self.footer_right])), 'footer')
loading = urwid.Text('loading...')
adapter = urwid.Filler(loading)
super(TextUserInterfaceWidget, self).__init__(body=adapter,
header=header,
footer=footer)
[docs]class TextUserInterface:
def __init__(self, exercise=None, *args, **kwargs):
self.exercise = exercise
self.arguments = kwargs
self.counter = 0
self.correct = 0
self.wrong = 0
palette = [
('default', 'default', 'default'),
('highlight', 'black', 'light gray'),
('header', 'light gray', 'dark blue', '', '#fff', '#336'),
('footer', 'light gray', 'dark blue', '', '#fff', '#336'),
]
self.tui_widget = TextUserInterfaceWidget(*args, **kwargs)
self.loop = urwid.MainLoop(widget=self.tui_widget, palette=palette)
self.loop.screen.set_terminal_properties(colors=256)
try:
with self.loop.start():
new_question = True
while(True):
if new_question:
self.counter += 1
self.create_question(self.exercise, **self.arguments)
self.run_question()
new_question = False
while len(self.input_keys) < self.question.n_input_notes:
input_key = self.loop.screen.get_input()[0]
# these inputs are answers to the exercise
if input_key in self.question.keyboard_index \
and input_key != ' ': # space char
self.input_keys.append(input_key)
if self.question.n_input_notes > 1:
self.update_input_display()
# these inputs are commands to birdears
else:
self.keypress(input_key)
if self.question.n_input_notes:
answer = self.input_keys
self.check_question(self.input_keys)
new_question = True
except urwid.ExitMainLoop:
print("Birdears <https://github.com/iacchus/birdears>")
print("Exiting...", end="\n\n")
print("Correct: {} / Wrong: {}".format(self.correct, self.wrong), end="\n\n")
[docs] def check_question(self, user_input):
answer = self.question.check_question(user_input)
# TODO: UPDATE DISPLAY BEFORE play_resolution
if answer['is_correct']:
self.correct += 1
self.question.display['main_display'] = 'Correct!'
else:
self.wrong += 1
self.question.display['main_display'] = 'Incorrect!'
answers_text = "Answers: +{correct} / -{incorrect} ".\
format(correct=self.correct, incorrect=self.wrong)
self.tui_widget.footer_right.set_text(answers_text)
self._draw_screen()
kwargs = {
'callback': self.keyboard.highlight_key,
'end_callback': self.keyboard.highlight_key,
}
self.question.play_resolution(**kwargs)
return answer
[docs] def create_question(self, exercise, **kwargs):
if exercise in QUESTION_CLASSES:
QUESTION_CLASS = QUESTION_CLASSES[exercise]
else:
raise Exception("Invalid `exercise` value:", exercise)
self.question = QUESTION_CLASS(**kwargs)
self.question.display.callback = self.update_question_display
self.input_keys = list()
self.draw_question()
[docs] def run_question(self):
kwargs = {
'callback': self.keyboard.highlight_key,
'end_callback': self.keyboard.highlight_key,
}
self.question.play_question(**kwargs)
self.keyboard.highlight_key(element=None)
# TYPES OF DISPLAYS:
#
# input_keys (transformed into intervals or notes)
# str() (displayed literally, overwriting)
# str() (showing now lines, deleting old ones)
# list() of str() (showing each str() on a new line)
# etc
# Maybe this chould well be an widget feeded by the display dict
[docs] def update_question_display(self):
for key, value in self.question.display.items():
if key not in self.question_widget.display:
self.question_widget.redraw_display(self.question.display)
self.question_widget.display[key].set_text(value)
self._draw_screen()
[docs] def update_input_display(self):
keyboard_index = self.question.keyboard_index
intervals = [INTERVALS[keyboard_index.index(item)][1]
for item in self.input_keys]
intervals_str = " ".join(intervals)
self.question.display.update({'input_display': intervals_str})
[docs] def draw_question(self):
if self.exercise != 'notename':
scale = \
ChromaticScale(tonic=self.question.lowest_tonic_pitch.note,
octave=self.question.lowest_tonic_pitch.octave,
descending=False,
n_octaves=self.question.n_octaves)
else:
scale = \
ChromaticScale(tonic='C',
octave=self.question.lowest_tonic_pitch.octave)
#descending=False,
#n_octaves=self.question.n_octaves)
self.keyboard = \
Keyboard(scale=scale,
question_tonic_pitch=self.question.tonic_pitch,
main_loop=self.loop,
keyboard_index=self.question.keyboard_index)
top_variables = {
'tonic': self.question.tonic_str,
'mode': self.question.mode.capitalize(),
'random': ('(random) ' if ('tonic' in self.arguments and
any(el in self.arguments['tonic']
for el in ('r', 'R'))) else ''),
'descending': self.question.is_descending,
'chromatic': self.question.is_chromatic,
'n_octaves': self.question.n_octaves,
'octaves_str': ('octave' if self.question.n_octaves == 1
else 'octaves'),
'intervals': ", ".join(self.question.allowed_intervals)
}
# eg.:
#
# Melodic Dictation
# Key: C Major Descending scale
# Octaves: 2
# Chromatics: True
# Number of notes: 4
#
# C Chromatic Descending scale
# 1 octave
top_text = "Key: {tonic} {mode} {random}({n_octaves} {octaves_str})\n"\
"Descending: {descending}\n" \
"Chromatic: {chromatic}\n" \
"Intervals: {intervals}" \
.format(**top_variables)
top_widget = urwid.Text(top_text)
self.question_widget = QuestionWidget(top_widget=top_widget,
keyboard=self.keyboard,
display=self.question.display)
self.tui_widget.contents.update({'body': (self.question_widget, None)})
if self.question.n_input_notes > 0:
answers_text = "Answers: +{correct} / -{incorrect} ".\
format(correct=self.correct, incorrect=self.wrong)
else:
answers_text = "(Question type doesn't takes answer)"
self.tui_widget.footer_right.set_text(answers_text)
with LOCK:
self.loop.draw_screen()
[docs] def keypress(self, key):
if key in ('T', 't'):
with LOCK:
self.loop.screen.clear()
self.loop.draw_screen()
elif key in ('R', 'r'):
self.run_question()
elif key in ('Q', 'q'):
raise urwid.ExitMainLoop()
elif key == 'backspace':
if len(self.input_keys) > 0:
self.input_keys.remove(self.input_keys[-1])
self.update_input_display()
pass
else:
pass
def _draw_screen(self):
with LOCK:
# self.loop.screen.clear()
self.loop.draw_screen()
raw_inpt = list(set(self.loop.screen.get_available_raw_input()))
for item in raw_inpt:
self.keypress(chr(item))