####################################################################################################
#
# Musica - A Music Theory Package for Python
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
r"""
String Vibration
----------------
There are four physical quantities involved in the string vibration phenomenon :
* the frequency :math:`f` of dimension :math:`s^{-1}`,
* the linear density :math:`\mu` of the string of dimension :math:`kg \cdot m^{-1}`,
* the tension :math:`T` applied to the string of dimension :math:`kg \cdot m \cdot s^{-2}`,
* the length :math:`L` of the string of dimension :math:`m`.
According to the Vaschy-Buckingham theorem, we can build a dimensionless constant:
.. math::
\alpha = \frac{T}{\mu L^2 f^2}
and thus
.. math::
f = \frac{\beta}{L} \sqrt{\frac{T}{\mu}}
where :math:`\beta` is a dimensionless constant.
In reality, the vibration of a string corresponds to the superposition of standing waves of
frequencies:
.. math::
f_n = \frac{n}{2L} \sqrt{\frac{T}{\mu}}
where :math:`n` is an integer greater than 0.
This series of frequencies is called an harmonic series. The first one is called fundamental
frequency, :math:`n=1`, and the others overtones.
We can notice from this formulae:
* the shorter the string, the higher the frequency of the fundamental,
* the higher the tension, the higher the frequency of the fundamental,
* the lighter the string, the higher the frequency of the fundamental.
Octave and Fifth
----------------
Let be a string of length L with a movable bridge. We denote :math:`f_0` the fundamental frequency
of the string.
* If we place the mobile bridge at the middle of the string, :math:`L/2`, the string fundamental
will now ring at :math:`f_8 = 2 \times f_0`, thus at the higher octave of :math:`f_0`.
* If we place the mobile bridge at :math:`2/3 L`, which is the next simpler subdivision of the
string, the larger string part will ring at :math:`f_5 = \frac{3}{2} \times f_0`, thus at the
higher perfect fifth of :math:`f_0`.
We define the fourth as the ratio of :math:`f_4 = f_8 / f_5 = \frac{4}{3}`, which is the complement
to the fifth to match the octave.
"""
####################################################################################################
import math
####################################################################################################
[docs]class Frequency:
##############################################
[docs] def __init__(self, frequency):
self._frequency = frequency
##############################################
[docs] def __repr__(self):
return "{0.__class__.__name__} {0._frequency}".format(self)
##############################################
@property
def frequency(self):
return self._frequency
@property
def period(self):
return 1 / self._frequency
@property
def pulsation(self):
return 2 * math.pi * self._frequency
##############################################
[docs] def __float__(self):
return float(self._frequency)
##############################################
[docs] def __eq__(self, other):
return self._frequency == other._frequency # Fixme: float() ?
[docs] def __lt__(self, other):
return self._frequency < float(other)
##############################################
[docs] def __truediv__(self, other):
return Cent.from_frequency(self, other)
####################################################################################################
[docs]class Cent:
##############################################
[docs] @classmethod
def from_frequency_ratio(cls, frequency_ratio):
cent = 1200 * math.log(frequency_ratio, 2)
return cls(cent)
##############################################
[docs] @classmethod
def from_frequency(cls, frequency1, frequency2):
cent = 1200 * math.log(float(frequency1) / float(frequency2), 2)
return cls(cent)
##############################################
[docs] def __init__(self, cent):
self._cent = cent
##############################################
[docs] def __repr__(self):
return "{0.__class__.__name__} {0._cent}".format(self)
##############################################
@property
def cent(self):
return self._cent
##############################################
[docs] def __float__(self):
return self._cent
##############################################
[docs] def __eq__(self, other):
return self._cent == other._cent # Fixme: float() ?
[docs] def __lt__(self, other):
return self._cent < float(other)
##############################################
[docs] def to_frequency(self, frequency):
return Frequency(float(frequency) * 2 ** (self._cent / 1200))
####################################################################################################
[docs]class FrequencyRatio:
unisson = 1
fourth = 4 / 3
fifth = 3 / 2
octave = 2
et12 = 2**(1/12)
####################################################################################################
[docs]class PythagoreanPitch:
##############################################
[docs] def __init__(self, numerator_power, denominator_power):
self._numerator_power = numerator_power
self._denominator_power = denominator_power
##############################################
[docs] def __repr__(self):
return "{0.__class__.__name__} {0.numerator}/{0.denominator}".format(self)
##############################################
@property
def numerator_power(self):
return self._numerator_power
@property
def denominator_power(self):
return self._denominator_power
@property
def cent(self):
return Cent.from_frequency_ratio(float(self))
##############################################
[docs] def __float__(self):
return self.numerator / self.denominator
##############################################
[docs] def __lt__(self, other):
return float(self) < float(other)
####################################################################################################
[docs]class PythagoreanFifth(PythagoreanPitch):
##############################################
@property
def numerator(self):
return 3**self._numerator_power
@property
def denominator(self):
return 2**self._denominator_power
##############################################
[docs] def __truediv__(self, other):
delta_numerator = abs(self.denominator_power - other.denominator_power)
delta_denominator = abs(self.numerator_power - other.numerator_power)
return delta_numerator, delta_denominator
####################################################################################################
[docs]class PythagoreanFourth(PythagoreanPitch):
##############################################
@property
def numerator(self):
return 2**self._numerator_power
@property
def denominator(self):
return 3**self._denominator_power
####################################################################################################
[docs]class PythagoreanTuningSingleton:
##############################################
[docs] @staticmethod
def generate_fifth_series():
# Generate a series of fifth
# https://fr.wikipedia.org/wiki/Accord_pythagoricien
# https://en.wikipedia.org/wiki/Pythagorean_tuning
pitchs = []
i = 0
octave_power = 0
stop = False
while not stop:
pitch = PythagoreanFifth(i, i + octave_power)
ratio = float(pitch)
if 2 <= ratio < 2.05: # 3**12 / 2**18 = 2.027
stop = True
elif ratio > 2:
octave_power += 1
pitch = PythagoreanFifth(i, i + octave_power)
pitchs.append(pitch)
i += 1
return pitchs
##############################################
[docs] @staticmethod
def add_fourths(fifth_series):
fifth_series = sorted(fifth_series)
fourth_series = []
for i, pitch in enumerate(fifth_series):
if 0 < i:
prev_pitch = fifth_series[i-1]
delta_numerator, delta_denominator = pitch / prev_pitch
if delta_numerator == 8: # apotome
fourth = PythagoreanFourth(pitch.denominator_power + 1, pitch.numerator_power)
fourth_series.append(fourth)
return fifth_series + fourth_series
##############################################
[docs] def __init__(self):
self._pitchs = sorted(self.add_fourths(self.generate_fifth_series()))
# Wolf interval = 7 octaves - 11 perfect fifths = 2**7 / (3/2)**11
self._wolf_interval = PythagoreanFourth(11 + 7, 11) # ~ 1.480 versus 1.5 for perfect fifth
##############################################
[docs] def __iter__(self):
return iter(self._pitchs)
##############################################
[docs] def __len__(self):
return len(self._pitchs)
##############################################
[docs] def __getitem__(self, i):
return self._pitchs[i]
##############################################
@property
def wolf_interval(self):
return self._wolf_interval
####################################################################################################
PythagoreanTuning = PythagoreanTuningSingleton()
####################################################################################################
[docs]class AdditionModuloGroup:
r"""Group of Addition Modulo a Positive Integer
Group Definition
A group is a set, G, together with an operation • (called the group law of G) that combines any
two elements a and b to form another element, denoted a • b or ab. To qualify as a group, the
set and operation, (G, •), must satisfy four requirements known as the group axioms:
Closure
For all a, b in G, the result of the operation, a • b, is also in G.
Associativity
For all a, b and c in G, (a • b) • c = a • (b • c).
Identity element
There exists an element e in G such that, for every element a in G, the equation e • a = a •
e = a holds. Such an element is unique.
Inverse element
For each a in G, there exists an element b in G, commonly denoted a−1 (or −a, if the
operation is denoted "+"), such that a • b = b • a = e, where e is the identity element.
Subgroup
H is a subgroup of G if the restriction of • to H × H is a group operation on H. This is usually
denoted H ≤ G, read as "H is a subgroup of G".
"""
##############################################
[docs] def __init__(self, modulo):
self._modulo = modulo
self._table = [[self.operation(i, j) for i in range(modulo)] for j in range(modulo)]
##############################################
@property
def modulo(self):
return self._modulo
@property
def cayley_table(self):
return self._table
##############################################
[docs] def operation(self, a, b):
return (a + b) % self._modulo
####################################################################################################
[docs]class ET12GroupSingleton(AdditionModuloGroup):
##############################################
[docs] def __init__(self):
super().__init__(modulo=12)
self._subgroups = (
# Fixme: unknown proof or algorithm to find them
# Pk = { i*k } for i,k and i*k in G
# 12 = 3 * 4 = 3 * 2 * 2 = 6 * 2
# k = 0 is C / Do
(0),
# k = 6 is tritone (C, F#) / (Do, Fa#)
(0, 6),
# k = 4 is augmented fifth chord (C, E, G#) / (Do, Mi, Sol#)
# triangle
(0, 4, 8),
# k = 3 is diminished seventh chord (C, Eb, F#, A) / (Do, Mib, Fa#, La)
# square
(0, 3, 6, 9),
# k = 2 is tone scale (C, D, E, F#, G#, Bb) / (Do, Ré, Mi, Fa#, Sol#, Sib)
# hexagon
(0, 2, 4, 6, 8, 10),
# k = 1 is G
# decagon
)
##############################################
@property
def subgroups(self):
return self._subgroups
####################################################################################################
ET12Group = ET12GroupSingleton()
####################################################################################################
[docs]class EqualTemperamentPitch:
##############################################
[docs] def __init__(self, step_number, number_of_steps):
self._step_number = step_number
self._number_of_steps = number_of_steps
##############################################
[docs] def __repr__(self):
return "{0.__class__.__name__} {0._step_number} / {0._number_of_steps}".format(self)
##############################################
@property
def step_number(self):
return self._step_number
@property
def number_of_steps(self):
return self._number_of_steps
@property
def cent(self):
return Cent.from_frequency_ratio(float(self))
##############################################
[docs] def __float__(self):
return 2**(self._step_number / self._number_of_steps)
##############################################
[docs] def __lt__(self, other):
# return float(self) < float(other)
return self._step_number < other.step_number
####################################################################################################
[docs]class EqualTemperamentTuning:
##############################################
[docs] @staticmethod
def fifth_approximations(number_of_steps_max=20):
r"""Compute the best perfect fifth approximations having the form :math:`2^{i/j}`.
12-TET is based on :math:`2^{7/12} \approx 1.498` versus 1.5 for the perfect fifth.
"""
approximations = []
for number_of_steps in range(1, number_of_steps_max +1):
for step_number in range(1, number_of_steps_max +1):
pitch = EqualTemperamentPitch(step_number, number_of_steps)
delta = abs(float(pitch) - FrequencyRatio.fifth)
if delta < .01:
approximations.append(pitch)
return approximations
##############################################
[docs] def __init__(self, number_of_steps, fifth_step_number):
# Waring: include octave !
self._number_of_steps = number_of_steps
self._fifth_step_number = fifth_step_number
self._pitchs = [EqualTemperamentPitch(step, number_of_steps)
for step in range(number_of_steps + 1)]
##############################################
[docs] def __iter__(self):
return iter(self._pitchs)
##############################################
[docs] def __len__(self):
return self._number_of_steps # len(self._pitchs)
##############################################
[docs] def __getitem__(self, i):
return self._pitchs[i]
##############################################
@property
def group(self):
return ET12Group
##############################################
@property
def number_of_steps(self):
return self._number_of_steps
##############################################
@property
def first_pitch(self):
return self._pitchs[0]
##############################################
@property
def fourth(self):
"""Return the fourth (F / Fa) which is the inversion of the fifth to the octave (5 = 12 - 7).
"""
fourth_step_number = self._number_of_steps - self._fifth_step_number
return self._pitchs[fourth_step_number]
##############################################
@property
def fifth(self):
return self._pitchs[self._fifth_step_number]
##############################################
[docs] def _fifth_series(self, number_of_iterations):
fifth_series = [(i*self._fifth_step_number) % self._number_of_steps
for i in range(1, number_of_iterations +1)]
return [self._pitchs[i] for i in fifth_series]
##############################################
@property
def fifth_series(self):
"""Return the complete fifth series up to the 7th octave.
(7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5, 0)
(G, D, A, E, B F#, C#, G#, D#, Bb, F, C)
(Sol, Ré, La, Mi, Si, Fa#, Do#, Sol#, Ré#, Sib, Fa, Do)
"""
return self._fifth_series(self._number_of_steps)
##############################################
@property
def fifth_series_for_major_scale(self):
"""Return the fifth series that constitute the major scale.
(7, 2, 9, 4, 11)
(G, D, A, E, B )
(Sol, Ré, La, Mi, Si)
and sorted
(2, 4, 7, 9, 11)
(D, E, G, A, B)
(Ré, Mi, Sol, La, Si)
Note the 6th fifth 6 / F#, is substituted by the fourth 5 / F since the major scale is made
of 7 notes, the first note, the fifth, the fourth, and 4 fifths to complete the set.
"""
return self._fifth_series(self._fifth_step_number -2) # 7 - 1 - 1
##############################################
@property
def major_scale(self):
"""Return the major scale.
The major scale is made of 7 notes, the first note, the fifth, the fourth, and 4 fifths to
complete the set.
(0, 2, 4, 5, 7, 9, 11)
(C, D, E, F, G, A, B)
(Do, Ré, Mi, Fa, Sol, La, Si)
"""
return sorted(self.fifth_series_for_major_scale + [self.first_pitch, self.fourth])
##############################################
@property
def perfect_steps(self):
return sorted([self.first_pitch, self.fourth, self.fifth])
####################################################################################################
ET12Tuning = EqualTemperamentTuning(number_of_steps=12, fifth_step_number=7)