def metric_denom(degree): ''' .. py:function:: metric_denom(degree) Calculate a metric for the consonance heatmap. This metric is just the denominator of the normalized ratio. It is also the default metric if none is specified. :param scale: The degree (a frequency ratio) :returns: The metric ''' normalized_degree = normalize_interval(degree) #y = sp.fraction(normalized_degree)[0] - sp.fraction(normalized_degree)[1] y = sp.fraction(normalized_degree)[1] return y
def metric_denom(ratio): '''Function that computes the denominator of the normalized ratio Parameters ---------- ratio: float Returns ------- y : float denominator of the normalized ratio ''' ratio = sp.Rational(ratio).limit_denominator(10000) normalized_degree = normalize_interval(ratio) y = int(sp.fraction(normalized_degree)[1]) return y
def sum_q_for_all_intervals(scale): ''' Calculate a metric for a scale. :param scale: The scale (i.e., a list of ``Rational`` s) :returns: The metric. Metric 5 is an estimate of scale consonance. It is summation of the denominators of the normalized distinct ratios of the scale. Smaller values are more consonant. ''' m5 = np.sum([ sp.fraction(normalize_interval(x))[1] for x in distinct_intervals(scale) ]) return {"sum_q_for_all_intervals": m5}
def create_euler_fokker_scale(intervals, multiplicities, octave=2, normalize=True): ''' Create a scale in the Euler-Fokker Genera :param intervals: The factors to use for the construction (usually prime numbers) :param multiplicities: The multiplicities of the factors (see below) :param octave: The formal octave :param normalize: If ``True``, normalize the intervals to the octave. ``intervals`` and ``multiplicities`` should both be lists of equal length. The entries in ``multiplicities`` give the number of each factor to use. Therefore the following: .. code:: intervals = [3,5,7] multiplicities = [1,1,1] scale = create_euler_fokker_scale(intervals, multiplicities) Will create a scale with one 3, one 5, and one 7 as generators. The above will produce the following scale: .. math:: \\left [ 1, \\frac{35}{32}, \\frac{5}{4}, \\frac{21}{16}, \\frac{3}{2}, \\frac{105}{64}, \\frac{7}{4}, \\frac{15}{8}, 2\\right ] Also note that the two statements will generate the same output: .. code:: intervals = [3,5,7] multiplicities = [2,2,1] scale1 = create_euler_fokker_scale(intervals, multiplicities) intervals = [3,3,5,5,7] multiplicities = [1,1,1,1,1] scale2 = create_euler_fokker_scale(intervals, multiplicities) scale1 == scale2 True ''' output = [] for index in range(len(intervals)): output = output + [intervals[index]] * multiplicities[index] output = [sp.Integer(x) for x in output] potential = list( itertools.chain(*[[x for x in itertools.combinations(output, r)] for r in range(1, len(output) + 1)])) output = [] for item in potential: if len(item) == 0: output = output + [item[0]] else: output = output + [reduce(operator.__mul__, item)] if normalize: output = [sp.Integer(1) ] + [normalize_interval(x, octave) for x in output] + [sp.Integer(octave)] else: output = [sp.Integer(1)] + [x for x in output] + [sp.Integer(octave)] output = sorted(set(output)) return output
def create_equal_interval_scale(generator_interval, scale_size=12, number_down_intervals=6, epsilon=None, sort=True, octave=2, remove_duplicates=True, normalize=True): ''' Create a scale with equal-interval tuning :param generator_interval: The interval to use for generation (``sympy`` value) :param scale_size: The number of degrees in the scale :param number_down_intervals: The number of inverted intervals to use in scale construction. :param epsilon: Rounding parameter. If set to ``None`` no rounding is done. Otherwise the scale degrees are rounded to the nearest epsilon :param sort: If ``True``, sort the output by degree size :param octave: The formal octave :param remove_duplicates: If ``True`` remove duplicate entries :param normalize: IF ``True``, normalize the degrees to the octave In general one should keep epsilon at ``None`` and perform and rounding outside the function. This is a base function from which several other scales are derived, including: * The Pythagorean scale A scale with a perfect fifth (3/2) as the generating interval .. math:: P_5 = \\frac{3}{2} * The quarter-comma meantone scale A scale in which the generating interval is a perfect fifth narrowed by one quarter of syntonic comma .. math:: P_5 = \\frac{\\frac{3}{2}}{\\sqrt[4]{\\frac{81}{80}}} * EDO Scales EDO scales can be generated from an appropriate selection of the fifth. For example, the 12-TET scale would use the fifth: .. math:: P_5 = \\sqrt[\\frac{7}{12}]{2} ''' down_intervals = number_down_intervals + 1 up_intervals = scale_size - down_intervals + 1 r_5 = generator_interval output = [] for index in range(up_intervals): x = r_5 ** index if normalize: output = output + [normalize_interval(x, octave)] else: output = output + [x] for index in range(down_intervals): x = (1/r_5) ** index if normalize: output = output + [normalize_interval(x, octave)] else: output = output + [x] output = output + [sp.Integer(octave)] if epsilon is not None: output = list(set([round(y/epsilon)* epsilon for y in output])) else: output = list(output) if sort: output = sorted(output) if remove_duplicates: output = sorted(set(output)) return output
def create_harmonic_scale(first_harmonic, last_harmonic, normalize=True, octave=2): ''' Create a harmonic scale :param first_harmonic: The first harmonic :param last_harmonic: The last harmonic :param normalize: If true, normalize the scale to an octave (2/1 by default, otherwise taken from ``octave``) :param octave: The definition of the formal octave. :returns: The scale As an example of use, a normalized scale constructed from harmonics 3 to 20: .. code:: scale = create_harmonic_scale(3,20) which yields: .. math:: \\left [ 1, \\frac{13}{12}, \\frac{7}{6}, \\frac{5}{4}, \\frac{4}{3}, \\frac{17}{12}, \\frac{3}{2}, \\frac{19}{12}, \\frac{5}{3}, \\frac{11}{6}, 2\\right ] To create a non-normalized scale: .. code:: scale = create_harmonic_scale(3,10, normalize=False) which yields: .. math:: \\left [ 1, \\frac{4}{3}, \\frac{5}{3}, 2, \\frac{7}{3}, \\frac{8}{3}, 3, \\frac{10}{3}\\right ] ''' r = [sp.Integer(1) + sp.Integer(1) * (i) for i in range(last_harmonic)] r = r[first_harmonic - 1:] r = [i / r[0] for i in r] output = [] for interval in r: if normalize: interval = normalize_interval(interval, octave) output = output + [interval] output = sorted(list(set(output))) if normalize: output = [sp.Integer(1) ] + [normalize_interval(x, octave) for x in output] + [sp.Integer(octave)] else: output = [sp.Integer(1)] + [x for x in output] + [sp.Integer(octave)] output = sorted(set(output)) return output
def create_lucy_tuning_spiral(scale_size=44, number_fourths=22, sort=False, octave=2): ''' Create the Lucy scale tuning spiral. The default values were chosen to match the published numbers :param scale_size: The number of intervals to produce :param number_fourth: The number of fourths to use :param sort: If True, the output will be sorted on scale size :param octave: The formal octave :returns: A list of tuples, the first member of which is the assigned symbol for the degree, and the second is the precise degree value (``sympy`` value). The tuning spiral is created by defining the Perfect Fifth and Perfect Fourth in terms of Lucy steps: .. math :: \\begin{align} P_5 & = L^3 \\cdot s \\cr P_4 &= L^2 \\cdot s \\end{align} The fourths and fifths are used as generators; in the default case, 22 of each are used to generate the target tones. **Important Note** According to the documentation of the scale, the fourths and fifths are taken in an opposite sense (which is to say one goes clockwise on the tuning circle, one goes counter-clockwise). However, based upon the published values *this is not really the case*. In this code both the fourths and the fifths are propagated in the same direction (no inversion is taken). The symbolic names are somewhat idiosyncratic and have been crafted by hand. ''' L = sp.root(2, 2 * sp.pi) s = sp.sqrt(2 / L**5) number_fifths = scale_size - number_fourths r_5 = L * L * L * s r_4 = L * L * s output = [] start = 1 running_total = 1 for index in range(number_fifths): scale_symbol = ('#' * (running_total // 7)) + str(start) x = r_5**index output = output + [(scale_symbol, normalize_interval(x, octave))] start = (start + 4) % 7 if start == 0: start = 7 running_total = running_total + 1 start = 1 running_total = 1 for index in range(number_fourths): scale_symbol = ('b' * ((running_total + 4) // 7)) + str(start) x = (r_4)**index output = output + [(scale_symbol, normalize_interval(x, octave))] start = (start + 3) % 7 if start == 0: start = 7 if start == 1: start = 8 running_total = running_total + 1 output = output + [("8", sp.Integer(octave))] output = list(output) if sort: output = sorted(set(output), key=lambda x: x[1]) return output
def test_normalize(self): self.assertEqual(sp.Rational(3, 2), normalize_interval(3)) self.assertEqual(sp.Rational(9, 8), normalize_interval(9)) self.assertEqual(sp.Rational(21, 16), normalize_interval(21))