Source code for birdears.resolution
from . import MAX_SEMITONES_RESOLVE_BELOW
from .interval import Interval
from .scale import DiatonicScale
from .sequence import Sequence
from .note_and_pitch import get_pitch_by_number
from .note_and_pitch import Chord
from functools import wraps
RESOLUTION_METHODS = {}
# http://stackoverflow.com/questions/5910703/howto-get-all-methods-of-a-pyt\
# hon-class-with-given-decorator
# http://stackoverflow.com/questions/5707589/calling-functions-by-array-ind\
# ex-in-python/5707605#5707605
[docs]def register_resolution_method(f, *args, **kwargs):
"""Decorator for resolution method functions.
Functions decorated with this decorator will be registered in the
`RESOLUTION_METHODS` global dict.
"""
@wraps(f)
def decorator(*args, **kwargs):
return f(*args, **kwargs)
RESOLUTION_METHODS.update({f.__name__: f})
return decorator
[docs]class Resolution:
"""This class implements methods for different types of question
resolutions.
A resolution is an answer to a question. It aims to create a mnemonic on
how the inverval resolves to the tonic.
"""
def __init__(self, method, question):
"""Inits the resolution class.
Args:
method (str): The method used in the resolution.
question (obj): Question object from which to generate the
resolution sequence.
"""
self.METHOD = RESOLUTION_METHODS[method]
self.question = question
def __call__(self, *args, **kwargs):
"""Calls the resolution method and pass arguments to it.
Returns a `birdears.Sequence` object with the resolution generated by
the.method.
"""
return self.METHOD(question=self.question, *args, **kwargs)
[docs]@register_resolution_method
def nearest_tonic(question):
"""Resolution method that resolve the intervals to their nearest tonics.
Args:
question (obj): Question object from which to generate the
resolution sequence. (this is provided by the `Prequestion` class
when it is `__call__`ed)
"""
tonic_pitch = question.tonic_pitch
# if hasattr(question, 'random_pitch'):
# random_pitch = tuple(question.random_pitch)
# else:
# random_pitch = tuple(question.random_pitches)
#
# TODO:
if hasattr(question, 'random_pitches'):
raise Exception('NEAREST_TONIC FOR MULTIPLE PITCHES IS STILL TO BE'
'IMPLEMENTED')
random_pitch = question.random_pitch
duration = question.durations['resol']['duration']
delay = question.durations['resol']['delay']
pos_delay = question.durations['resol']['pos_delay']
# this function will receive: tonic, scale and random_pitch (which may be
# chromatic, ie., not in `scale`)
semitones = (int(random_pitch) - int(tonic_pitch)) % 12
scale_random_pitch = question.diatonic_scale
direction = -1 if (semitones <= MAX_SEMITONES_RESOLVE_BELOW) else +1
# invert `direction` signal if descending:
pitch_direction = (direction * -1) if question.is_descending else direction
resolution = []
if random_pitch not in scale_random_pitch: # random_pitch is chromatic
resolution.append(random_pitch)
# if this note is chromatic then the
# next ones above or below are in the diatonic for sure, so we
# add one semitone and we will have the next diatonic degree:
nearest_diatonic_pitch = \
get_pitch_by_number(int(random_pitch) + direction)
else:
nearest_diatonic_pitch = random_pitch # random_pitch is diatonic
resolve_mask = 0 if semitones <= MAX_SEMITONES_RESOLVE_BELOW else 12
nearest_tonic_pitch = get_pitch_by_number(int(random_pitch) +
(resolve_mask-semitones))
nearest_tonic_index = scale_random_pitch.index(nearest_tonic_pitch)
nearest_diatonic_pitch_index = \
scale_random_pitch.index(nearest_diatonic_pitch)
random_pitch_index = nearest_diatonic_pitch_index
ohslice = slice(min(nearest_tonic_index, random_pitch_index),
max(nearest_tonic_index, nearest_diatonic_pitch_index)+1)
resolution = scale_random_pitch[ohslice][::pitch_direction]
# is it chromatic?
if random_pitch not in scale_random_pitch:
resolution.insert(0, random_pitch)
if len(resolution) == 1:
resolution.append(tonic_pitch)
if question.is_harmonic:
resolution_harmonic = []
for item in resolution:
resolution_harmonic.append(Chord([tonic_pitch, item]))
resolution = resolution_harmonic
# resolution
return Sequence(elements=resolution, duration=duration, delay=delay,
pos_delay=pos_delay)
[docs]@register_resolution_method
# FIXME : it should both play preq and question
def repeat_only(question):
"""Resolution method that only repeats the sequence elements with given
durations.
Args:
question (obj): Question object from which to generate the
resolution sequence. (this is provided by the `Prequestion` class
when it is `__call__`ed)
"""
elements = list()
if hasattr(question, 'random_pitches'):
elements.extend(question.random_pitches)
else:
elements.append(question.random_pitch)
elements.append(question.tonic_pitch)
duration = question.durations['resol']['duration']
delay = question.durations['resol']['delay']
pos_delay = question.durations['resol']['pos_delay']
sequence_list = Sequence(elements, duration=duration, delay=delay,
pos_delay=pos_delay)
return sequence_list