from random import randrange
from random import choice
from . import D
from . import KEYBOARD_INDICES
from . import CHROMATIC_TYPE
from . import KEYS
from . import DEGREE_INDEX
from . import INTERVALS
from . import DIATONIC_FORMS
# from .interval import Interval
from .note_and_pitch import Pitch
from .note_and_pitch import get_pitch_by_number
from .scale import DiatonicScale
from .scale import ChromaticScale
from .utils import DictCallback
from functools import wraps
QUESTION_CLASSES = {}
[docs]def register_question_class(cls, *args, **kwargs):
"""Decorator for question classes.
Classes decorated with this decorator will be registered in the
`QUESTION_CLASSES` global.
"""
global QUESTION_CLASSES
QUESTION_CLASSES.update({cls.name: cls})
return cls
# values for valid_semitones list can be Interval objects or int's (semitones)
[docs]def get_valid_pitches(scale, valid_intervals=CHROMATIC_TYPE):
tonic_pitch = scale[0]
valid_scale = list()
if isinstance(valid_intervals, tuple):
valid_list = list(map(lambda x: str(x), valid_intervals))
elif isinstance(valid_intervals, list):
valid_list = list(map(lambda x: str(x), valid_intervals))
elif isinstance(valid_intervals, str):
valid_list = valid_intervals.replace(' ', '').split(',')
else:
raise Exception('Incorrect type for valid_semitones')
valid_semitones = list()
for item in valid_list:
# 'i', 'ii' etc...
if item.lower() in DEGREE_INDEX:
valid_semitones.extend(DEGREE_INDEX[item.lower()])
# 0, 1, 2 etc...
elif item.isdecimal():
valid_semitones.append(int(item))
# something else
else:
print('Warning: invalid `valid_interval`: ', item)
continue
for pitch in scale:
# this will work with multple octaves
chromatic_offset = \
abs(int(tonic_pitch) - int(pitch)) % 12
if chromatic_offset in valid_semitones:
valid_scale.append(pitch)
return valid_scale
[docs]class QuestionBase:
"""
Base Class to be subclassed for Question classes.
This class implements attributes and routines to be used in Question
subclasses.
"""
def __init__(self, mode='major', tonic='C', octave=4, descending=False,
chromatic=False, n_octaves=1, valid_intervals=CHROMATIC_TYPE,
user_durations=None, prequestion_method=None,
resolution_method=None, default_durations=None,
*args, **kwargs):
"""Inits the class.
Args:
mode (str): A string represnting the mode of the question.
Eg., 'major' or 'minor'
tonic (str): A string representing the tonic of the
question, eg.: 'C'; if omitted, it will be selected
randomly.
octave (int): A scienfic octave notation, for example,
4 for 'C4'; if not present, it will be randomly chosen.
descending (bool): Is the question direction in descending,
ie., intervals have lower pitch than the tonic.
chromatic (bool): If the question can have (True) or not
(False) chromatic intervals, ie., intervals not in the
diatonic scale of tonic/mode.
n_octaves (int): Maximum numbr of octaves of the question.
valid_intervals (list): A list with intervals (int) valid for
random choice, 1 is 1st, 2 is second etc. Eg. [1, 4, 5] to
allow only tonics, fourths and fifths.
user_durations (str): A string with 9 comma-separated `int` or
`float`s to set the default duration for the notes played. The
values are respectively for: pre-question duration (1st),
pre-question delay (2nd), and pre-question pos-delay (3rd);
question duration (4th), question delay (5th), and question
pos-delay (6th); resolution duration (7th), resolution
delay (8th), and resolution pos-delay (9th).
duration is the duration in of the note in seconds; delay is
the time to wait before playing the next note, and pos_delay is
the time to wait after all the notes of the respective sequence
have been played. If any of the user durations is `n`, the
default duration for the type of question will be used instead.
Example::
"2,0.5,1,2,n,0,2.5,n,1"
prequestion_method (str): Method of playing a cadence or the
exercise tonic before the question so to affirm the question
musical tonic key to the ear. Valid ones are registered in the
`birdears.prequestion.PREQUESION_METHODS` global dict.
resolution_method (str): Method of playing the resolution of an
exercise Valid ones are registered in the
`birdears.resolution.RESOLUTION_METHODS` global dict.
user_durations (dict): Dictionary with the default durations for
each type of sequence. This is provided by the subclasses.
"""
self.display = DictCallback({'main_display': str()})
if isinstance(mode, str) and any(el == mode for el in ('R', 'r')):
mode = choice(list(DIATONIC_FORMS))
self.mode = mode
self.is_descending = descending
self.is_chromatic = chromatic
try:
if kwargs['n_notes']:
self.n_notes = kwargs['n_notes']
except KeyError:
self.n_notes = 1
self.n_input_notes = int(self.n_notes)
# self.octave = octave if octave else randrange(3, 5)
if isinstance(octave, str) and any(el in octave for el in ('R', 'r')):
self.octave = randrange(3, 6)
elif isinstance(octave, str) and ',' in octave:
octave = octave.replace(' ', '')
octave = choice(octave.split(','))
self.octave = int(octave)
elif isinstance(octave, int) and (octave >= 3 and octave <= 6):
self.octave = octave
elif isinstance(octave, list):
self.octave = int(choice(octave))
elif isinstance(octave, tuple) and len(octave) == 2:
self.octave = randrange(*octave)
# if not octave:
else:
self.octave = randrange(3, 6)
# TODO: raise exceptions in case octave/n_octaves are invalid or
# extrapolate each other
# self.octave = octave
self.n_octaves = n_octaves
direction = 'descending' if descending else 'ascending'
# FIXME: maybe this should go to __main__
self.keyboard_index = \
tuple(KEYBOARD_INDICES['chromatic'][direction][self.mode])
if isinstance(tonic, list) or isinstance(tonic, tuple):
tonic = choice(tonic)
elif isinstance(tonic, str) and ',' in tonic:
tonic = tonic.replace(' ', '')
tonic = choice(tonic.split(','))
elif isinstance(tonic, str) and ('R' in tonic or 'r' in tonic):
tonic = choice(KEYS)
self.tonic_pitch = Pitch(note=tonic, octave=self.octave)
self.tonic_str = str(self.tonic_pitch.note)
self.tonic_pitch_str = str(self.tonic_pitch)
if not chromatic:
self.scale = DiatonicScale(tonic=self.tonic_str, mode=mode,
octave=self.octave,
descending=descending,
n_octaves=n_octaves)
else:
self.scale = ChromaticScale(tonic=self.tonic_str,
octave=self.octave,
descending=descending,
n_octaves=n_octaves)
self.diatonic_scale = DiatonicScale(tonic=self.tonic_str, mode=mode,
octave=self.octave,
descending=descending,
n_octaves=n_octaves)
self.chromatic_scale = ChromaticScale(tonic=self.tonic_str,
octave=self.octave,
descending=descending,
n_octaves=n_octaves)
self.tonic_accident = ('flat' if (('b' in self.tonic_str)
or (self.tonic_str == 'F'))
else 'sharp')
if self.is_descending:
self.lowest_tonic_pitch = \
get_pitch_by_number(int(self.tonic_pitch)
- (self.n_octaves * 12),
accident=self.tonic_accident)
else:
self.lowest_tonic_pitch = self.tonic_pitch
#D(self.lowest_tonic_pitch)
self.allowed_pitches = \
get_valid_pitches(self.scale, valid_intervals=valid_intervals)
self.allowed_intervals = \
[INTERVALS[abs(int(self.tonic_pitch)-int(pitch))][1]
for pitch in self.allowed_pitches]
self.durations = default_durations
if user_durations:
ud_index = {
0: ('preq', 'duration'),
1: ('preq', 'delay'),
2: ('preq', 'pos_delay'),
3: ('quest', 'duration'),
4: ('quest', 'delay'),
5: ('quest', 'pos_delay'),
6: ('resol', 'duration'),
7: ('resol', 'delay'),
8: ('resol', 'pos_delay'),
}
ud = user_durations.split(',')
if len(ud) == len(ud_index):
for idx, v in ud_index.items():
cur_duration = ud[idx].strip()
if cur_duration != 'n':
self.durations[v[0]][v[1]] = float(cur_duration)
self.prequestion_method = prequestion_method
self.resolution_method = resolution_method
[docs] def make_question(self):
"""This method should be overwritten by the question subclasses.
"""
pass
[docs] def make_resolution(self):
"""This method should be overwritten by the question subclasses.
"""
pass
[docs] def play_question(self):
"""This method should be overwritten by the question subclasses.
"""
pass
[docs] def check_question(self):
"""This method should be overwritten by the question subclasses.
"""
pass