Source code for Musica.Figure.Fretboard

####################################################################################################
#
# 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/>.
#
####################################################################################################

####################################################################################################

__all__ = [
    'Fretboard',
]

####################################################################################################

import math

from IntervalArithmetic import IntervalInt

from ..Geometry.Path import Polyline
from ..Geometry.Transformation import AffineTransformation2D
from ..Geometry.Vector import Vector2D
# from ..Instrument.InstrumentDatabase import InstrumentDatabase
from ..Instrument.StringInstrument import StringInstrumentTuningDatabase, String # , StringTuning
from ..Tex.Tikz import TikzFigure, Coordinate
from ..Theory.Pitch import Pitch

####################################################################################################

class StringFretboard:

    ##############################################

    def __init__(self, string, number_of_frets):

        self._string = string
        self._number_of_frets = number_of_frets

        pitch = string.pitch
        self._pitches = []
        for i in range(self._number_of_frets +1):
            self._pitches.append(pitch)
            pitch = pitch.next_pitch()

    ##############################################

    @property
    def string(self):
        return self._string

    @property
    def number_of_frets(self):
        return self._number_of_frets

    @property
    def pitch(self):
        return self._pitches[0]

    # alias
    first_pitch = pitch

    @property
    def last_pitch(self):
        return self._pitches[-1]

    @property
    def pitch_interval(self):
        # /!\ midi note, i.e. int not float
        return IntervalInt(int(self.first_pitch), int(self.last_pitch))

    ##############################################

    def __getitem__(self, i):
        return self._pitches[i]

####################################################################################################

[docs]class Fretboard: ##############################################
[docs] def __init__(self, **kwargs): tuning = kwargs['tuning'] if isinstance(tuning, str): # not StringTuning tuning_database = StringInstrumentTuningDatabase.instance() instrument = kwargs['instrument'] tuning = tuning_database[instrument][tuning] # Fixme: reverse = kwargs.get('reverse', False) # Fixme: Guitar default self._diapason = kwargs.get('diapason', 640) # mm self._number_of_frets = kwargs.get('number_of_frets', 12) self._number_of_strings = tuning.number_of_strings self._strings = [StringFretboard(String(tuning[i], self._diapason), self._number_of_frets) for i in self.string_iter] string0 = self._strings[0].string self._fret_positions = [string0.fret_position(i) for i in self.nut_fret_iter]
############################################## @property def number_of_strings(self): return self._number_of_strings @property def diapason(self): return self._diapason @property def number_of_frets(self): return self._number_of_frets @property def string_iter(self): return range(self._number_of_strings) @property def string_fretboard_iter(self): return iter(self._strings) @property def nut_fret_iter(self): return range(self._number_of_frets + 1) @property def fret_iter(self): return range(1, self._number_of_frets + 1) @property def x_last_fret(self): return self._fret_positions[-1] ##############################################
[docs] def string(self, i): return self._strings[i]
##############################################
[docs] def fret_position(self, i): return self._fret_positions[i]
##############################################
[docs] def middle_position(self, i): return (self._fret_positions[i-1] + self._fret_positions[i]) / 2
#################################################################################################### class PitchInterval: ############################################## @classmethod def make_C_E_interval(cls, octave): lower_pitch = Pitch('C', octave=octave) upper_pitch = Pitch('E', octave=octave) return cls(lower_pitch, upper_pitch) ############################################## @classmethod def make_F_B_interval(cls, octave): lower_pitch = Pitch('F', octave=octave) upper_pitch = Pitch('B', octave=octave) return cls(lower_pitch, upper_pitch) ############################################## def __init__(self, lower_pitch, upper_pitch): self._lower_pitch = lower_pitch self._upper_pitch = upper_pitch self._interval = IntervalInt(int(lower_pitch), int(upper_pitch)) ############################################## @property def lower_pitch(self): return self._lower_pitch @property def upper_pitch(self): return self._upper_pitch @property def interval(self): return self._interval ############################################## def __len__(self): return self._interval.length ############################################## def __repr__(self): return '{0._lower_pitch} {0._upper_pitch}'.format(self) ############################################## def __contains__(self, pitch): return int(pitch) in self._interval ############################################## def length_from(self, pitch): if pitch in self: delta = int(pitch) - self._interval.inf print(pitch, self._interval.length, delta) return self._interval.length - delta else: raise ValueError("Invalid pitch {} for {}".format(pitch, self)) #################################################################################################### class PitchIntervals: ############################################## def __init__(self, lower_pitch, upper_pitch): # Split in block (C, D, E) (F, G, A, B) self._pitch_intervals = [] octave = lower_pitch.octave if lower_pitch.degree < 3: pitch_interval = PitchInterval.make_C_E_interval(octave) is_C_E = True else: pitch_interval = PitchInterval.make_F_B_interval(octave) octave += 1 is_C_E = False self._pitch_intervals.append(pitch_interval) while pitch_interval.upper_pitch < upper_pitch: if is_C_E: pitch_interval = PitchInterval.make_F_B_interval(octave) octave += 1 else: pitch_interval = PitchInterval.make_C_E_interval(octave) is_C_E = not is_C_E self._pitch_intervals.append(pitch_interval) ############################################## def __len__(self): return len(self._pitch_intervals) ############################################## def __iter__(self): return iter(self._pitch_intervals) ############################################## def matchings_interval(self, lower_pitch, upper_pitch): # Faster algo: compute delta ??? pitch_intervals = [] start = True for interval in self._pitch_intervals: if start: if lower_pitch in interval: pitch_intervals.append(interval) start = False else: pitch_intervals.append(interval) if upper_pitch in interval: break return pitch_intervals #################################################################################################### class FretboardFigure(TikzFigure, Fretboard): ############################################## def __init__(self, **kwargs): Fretboard.__init__(self, **kwargs) # for i in self.string_iter: # string1_interval = self.string(i).pitch_interval # for j in self.string_iter: # if j != i: # string2_interval = self.string(j).pitch_interval # if string1_interval.intersect(string2_interval): # intersection = string1_interval & string2_interval # print(i, j, string1_interval, string2_interval, '&', intersection, 'P', ' '.join([str(x) for x in new_parts])) fretboard_interval = None for string in self.string_fretboard_iter: if fretboard_interval is None: fretboard_interval = string.pitch_interval.clone() else: fretboard_interval |= string.pitch_interval lower_pitch = Pitch(midi=fretboard_interval.inf) upper_pitch = Pitch(midi=fretboard_interval.sup) pitch_intervals = PitchIntervals(lower_pitch, upper_pitch) # Fixme: scale, paper TikzFigure.__init__(self, options='x=.5mm,y=5mm') self.set_main_font('Latin Modern Sans') # Roman self.font_size(4) # Fixme: broken corner # Mark # circle: 3, 5, 7, 9, 15, 17 # 2 circles: 12 delta_hue = 360 / len(pitch_intervals) hue = delta_hue even = True for i, pitch_interval in enumerate(pitch_intervals): name = 'color{}'.format(i) if even: hue = (i+1) * delta_hue else: hue = (i-1) * delta_hue hue = hue % 360 self.append(self.format(r'\definecolor{«»}{Hsb}{«»,0.3,1}', name, self.format_number(hue))) # Fixme: pitch_interval.colour = name even = not even # Paint pitch intervals for i, string in enumerate(self.string_fretboard_iter): y = i string_pitch_intervals = pitch_intervals.matchings_interval(string.first_pitch, string.last_pitch) lower_fret = 0 for interval in string_pitch_intervals: interval_length = interval.length_from(string[lower_fret]) upper_fret = min(lower_fret + interval_length -1, self.number_of_frets) if lower_fret: lower_x = self.fret_position(lower_fret-1) else: lower_x = '-10em' # print(i, interval, interval_length, lower_fret, upper_fret) upper_x = self.fret_position(upper_fret) point0 = Coordinate(lower_x, y) point1 = Coordinate(upper_x, y+1) self.rectangle(point0, point1, fill=interval.colour) lower_fret = upper_fret +1 # Paint string borders for i in range(self.number_of_strings +1): y = i if i == 0 or i == self.number_of_strings: linewidth = '1pt' else: linewidth = '.5pt' point0 = Vector2D(0, i) point1 = Vector2D(self.x_last_fret, i) self.line(point0, point1, linewidth=linewidth) # Paint frets for i in self.nut_fret_iter: x = self.fret_position(i) point0 = Vector2D(x, 0) point1 = Vector2D(x, self.number_of_strings) if i == 0: linewidth = '4pt' else: linewidth = '1pt' self.line(point0, point1, linewidth=linewidth) # Paint fret numbers for i in self.fret_iter: x_middle = self.middle_position(i) point = Vector2D(x_middle, -.25) self.text(point, i, anchor='north') # Paint pitches for i in self.string_iter: y = i y_middle = y + .5 string = self.string(i) # string pitch point = Coordinate('-7em', y_middle) self.text(point, i+1, anchor='east') point = Coordinate('-1em', y_middle) pitch = string.pitch english_pitch = pitch.unicode_name_with_octave latin_pitch = pitch.latin_unicode_name_with_octave self.text(point, '{} / {}'.format(english_pitch, latin_pitch), anchor='east') # fret pitches for i in self.fret_iter: fret_pitch = string[i] x = self.middle_position(i) point = Coordinate(x, y_middle) english_pitch = fret_pitch.unicode_name_with_octave latin_pitch = fret_pitch.latin_unicode_name self.text(point, '{} / {}'.format(english_pitch, latin_pitch)) # Paint decorations for i in (5, 7, 9): # 12 15 17 x = self.middle_position(i) point = Coordinate(x, self.number_of_strings / 2) self.circle(point, radius='1mm', fill=True) x = self.fret_position(12) for y in (-1, 1): point = Coordinate(x, (.5 + y * .1) * self.number_of_strings) self.circle(point, radius='1mm', fill=True)