Source code for music21.clef

# ------------------------------------------------------------------------------
# Name:         clef.py
# Purpose:      Objects for representing clefs
#
# Authors:      Michael Scott Asato Cuthbert
#               Christopher Ariza
#               Michael Bodenbach
#
# Copyright:    Copyright © 2009-2024 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
This module defines numerous subclasses of
:class:`~music21.clef.Clef`, providing object representations for all
commonly used clefs. Clef objects are often found
within :class:`~music21.stream.Measure` objects.
'''
from __future__ import annotations

from collections.abc import Iterable, Sequence
import typing as t
import unittest

from music21 import base
from music21 import exceptions21
from music21 import environment
from music21 import pitch
from music21 import style


if t.TYPE_CHECKING:
    from music21 import stream


environLocal = environment.Environment('clef')

TREBLE_MID_LINE_DNN = 35


class ClefException(exceptions21.Music21Exception):
    pass


# ------------------------------------------------------------------------------
[docs] class Clef(base.Music21Object): ''' A Clef is a basic `music21` object for representing musical clefs (Treble, Bass, etc.). Some clefs only represent the graphical element of the clef, such as G clef, which is subclassed by TrebleClef() and FrenchViolinClef(). >>> tc = clef.TrebleClef() >>> tc <music21.clef.TrebleClef> >>> tc.sign 'G' >>> tc.line 2 Most clefs also have a "lowestLine" function which represents the :attr:`~music21.pitch.Pitch.diatonicNoteNum` of the note that would fall on the lowest line if the Clef were put on a five-line staff. (Where C4,C#4,C##4,C-4 etc. = 29, all types of D4 = 30, etc.) >>> tc.lowestLine 31 **Equality** Two Clefs are equal if their class is the same, their sign is the same, their line is the same and their octaveChange is the same. >>> c1 = clef.PercussionClef() >>> c2 = clef.NoClef() >>> c1 == c2 False >>> c3 = clef.TrebleClef() >>> c4 = clef.TrebleClef() >>> c3 == c4 True >>> c4.octaveChange = -1 >>> c3 == c4 False Note that these are not equal: >>> clef.TrebleClef() == clef.GClef(line=2) False ''' equalityAttributes = ('sign', 'line', 'octaveChange') _DOC_ATTR: dict[str, str] = { 'sign': ''' The sign of the clef, generally, 'C', 'G', 'F', 'percussion', 'none' or None. >>> alto = clef.AltoClef() >>> alto.sign 'C' >>> percussion = clef.PercussionClef() >>> percussion.sign 'percussion' Note the difference here: >>> clef.Clef().sign is None True >>> clef.NoClef().sign 'none' ''', 'line': ''' The line, counting from the bottom up, that the clef resides on. >>> clef.AltoClef().line 3 >>> clef.TenorClef().line 4 May be None: >>> print(clef.NoClef().line) None ''', } _styleClass = style.TextStyle classSortOrder = 0 def __init__(self, **keywords) -> None: super().__init__(**keywords) self.sign: str|None = None # line counts start from the bottom up, the reverse of musedata self.line: int|None = None self._octaveChange: int = 0 # set to zero as default # musicxml has an attribute for clefOctaveChange, # an integer to show transposing clef def _reprInternal(self): return '' @property def octaveChange(self) -> int: ''' The number of octaves that the clef "transposes", generally 0. >>> tc = clef.TrebleClef() >>> tc.octaveChange 0 >>> clef.Treble8vbClef().octaveChange -1 ''' return self._octaveChange @octaveChange.setter def octaveChange(self, newValue: int): self._octaveChange = newValue @property def name(self) -> str: ''' Returns the "name" of the clef, from the class name >>> tc = clef.TrebleClef() >>> tc.name 'treble' >>> tc = clef.Treble8vbClef() >>> tc.name 'treble8vb' >>> tc = clef.MezzoSopranoClef() >>> tc.name 'mezzoSoprano' OMIT_FROM_DOCS >>> clef.Clef().name '' ''' className = self.__class__.__name__.replace('Clef', '') if className: return className[0].lower() + className[1:] else: return ''
[docs] def getStemDirectionForPitches( self, pitches: pitch.Pitch|Sequence[pitch.Pitch], *, firstLastOnly: bool = True, extremePitchOnly: bool = False, ) -> str: # noinspection PyShadowingNames ''' Return a string representing the stem direction for a single :class:`~music21.pitch.Pitch` object or a list/tuple/Stream of pitches. >>> P = pitch.Pitch >>> bc = clef.BassClef() >>> bc.getStemDirectionForPitches(P('C3')) 'up' For two pitches, the most extreme pitch determines the direction: >>> pitchList = [P('C3'), P('B3')] >>> bc.getStemDirectionForPitches(pitchList) 'down' If `firstLastOnly` is True (as by default) then only the first and last pitches are examined, as in a beam group. Here we have C3, B3, C3, so despite the B in bass clef being much farther from the center line than either of the Cs, it is stem up: >>> pitchList.append(P('C3')) >>> bc.getStemDirectionForPitches(pitchList) 'up' If `firstLastOnly` is False, then each of the pitches has a weight on the process >>> bc.getStemDirectionForPitches(pitchList, firstLastOnly=False) 'down' If extremePitchOnly is True, then whatever pitch is farthest from the center line determines the direction, regardless of order. (default False). >>> bc.getStemDirectionForPitches(pitchList, extremePitchOnly=True) 'down' >>> pitchList.insert(1, P('C2')) >>> bc.getStemDirectionForPitches(pitchList, extremePitchOnly=True) 'up' ''' pitchList: Sequence[pitch.Pitch] if isinstance(pitches, pitch.Pitch): pitchList = [pitches] else: pitchList = pitches relevantPitches: Sequence[pitch.Pitch] if not pitchList: raise ValueError('getStemDirectionForPitches cannot operate on an empty list') if extremePitchOnly: pitchMin = min(pitchList, key=lambda pp: pp.diatonicNoteNum) pitchMax = max(pitchList, key=lambda pp: pp.diatonicNoteNum) relevantPitches = [pitchMin, pitchMax] elif firstLastOnly and len(pitchList) > 1: relevantPitches = [pitchList[0], pitchList[-1]] else: relevantPitches = pitchList differenceSum = 0 # pylint: disable-next=no-member if isinstance(self, (PercussionClef, PitchClef)) and self.lowestLine is not None: midLine = self.lowestLine + 4 # pylint: disable=no-member else: midLine = TREBLE_MID_LINE_DNN # assume TrebleClef-like. for p in relevantPitches: distanceFromMidLine = p.diatonicNoteNum - midLine differenceSum += distanceFromMidLine if differenceSum >= 0: return 'down' else: return 'up'
# ------------------------------------------------------------------------------
[docs] class PitchClef(Clef): ''' Superclass for all other clef subclasses that use pitches. ''' _DOC_ATTR: dict[str, str] = { 'lowestLine': ''' The diatonicNoteNumber of the lowest line of the clef. (Can be `None`) >>> clef.TrebleClef().lowestLine 31 ''', } def __init__(self, **keywords) -> None: super().__init__(**keywords) self.lowestLine: int = 31 @property def octaveChange(self) -> int: ''' The number of octaves that the clef "transposes", generally 0. >>> tc = clef.TrebleClef() >>> tc.octaveChange 0 >>> clef.Treble8vbClef().octaveChange -1 Changing octaveChange changes lowestLine (but not vice versa) >>> tc.lowestLine 31 >>> tc.octaveChange = 1 >>> tc.lowestLine 38 >>> tc.octaveChange = -1 >>> tc.lowestLine 24 ''' return super().octaveChange @octaveChange.setter def octaveChange(self, newValue: int): oldOctaveChange = self._octaveChange self._octaveChange = newValue if self.lowestLine is not None: self.lowestLine += (newValue - oldOctaveChange) * 7
[docs] class PercussionClef(Clef): ''' Represents a Percussion clef. >>> pc = clef.PercussionClef() >>> pc.sign 'percussion' >>> pc.line is None True Percussion clefs should not, technically have a "lowestLine," but it is a common usage to assume that in pitch-centric contexts to use the pitch numbers from treble clef for percussion clefs. Thus: >>> pc.lowestLine == clef.TrebleClef().lowestLine True * Changed in v7.3: setting `octaveChange` no longer affects `lowestLine`. ''' _DOC_ATTR: dict[str, str] = {} def __init__(self, **keywords): super().__init__(**keywords) self.sign = 'percussion' self.lowestLine = (7 * 4) + 3 # 4 octaves + 3 notes = e4
[docs] class NoClef(Clef): ''' Represents the absence of a Clef. >>> nc = clef.NoClef() >>> nc.sign 'none' Note that the sign is the string 'none' not the None object >>> nc.sign is None False ''' _DOC_ATTR: dict[str, str] = {} def __init__(self, **keywords): super().__init__(**keywords) self.sign = 'none'
[docs] class JianpuClef(NoClef): ''' Jianpu notation does not use a clef, but musicxml marks it with a specialized "jianpu" sign. >>> jc = clef.JianpuClef() >>> jc.sign 'jianpu' ''' def __init__(self, **keywords): super().__init__(**keywords) self.sign = 'jianpu'
[docs] class TabClef(PitchClef): ''' Represents a Tablature clef. >>> a = clef.TabClef() >>> a.sign 'TAB' ''' def __init__(self, **keywords): super().__init__(**keywords) self.sign = 'TAB' self.line = 5
[docs] def getStemDirectionForPitches( self, pitchList: pitch.Pitch|Iterable[pitch.Pitch], *, firstLastOnly: bool = True, extremePitchOnly: bool = False, ) -> str: ''' Overridden to simply return 'down' for guitar tabs. ''' return 'down'
# ------------------------------------------------------------------------------
[docs] class GClef(PitchClef): ''' A generic G Clef. >>> a = clef.GClef() >>> a.sign 'G' If not defined, the lowestLine is set as a Treble Clef (E4 = 31). >>> a.lowestLine 31 ''' def __init__(self, **keywords): super().__init__(**keywords) self.sign = 'G'
[docs] class FrenchViolinClef(GClef): ''' A G Clef that appears in many old French Violin scores, appearing on the lowest line, and thus higher than a treble clef. >>> a = clef.FrenchViolinClef() >>> a.sign 'G' >>> a.line 1 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 1 self.lowestLine = (7 * 4) + 5
[docs] class TrebleClef(GClef): ''' The most common clef of all, a treble clef. >>> a = clef.TrebleClef() >>> a.sign 'G' >>> a.line 2 >>> a.lowestLine 31 >>> note.Note('E4').pitch.diatonicNoteNum 31 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 2 self.lowestLine = (7 * 4) + 3 # 4 octaves + 3 notes = e4
[docs] class Treble8vbClef(TrebleClef): ''' A vocal tenor treble clef. Also for guitars. >>> a = clef.Treble8vbClef() >>> a.sign 'G' >>> a.octaveChange -1 ''' def __init__(self, **keywords): super().__init__(**keywords) self.octaveChange = -1 self.lowestLine = (7 * 3) + 3
[docs] class Treble8vaClef(TrebleClef): ''' A treble clef an octave up (such as for piccolos). >>> a = clef.Treble8vaClef() >>> a.sign 'G' >>> a.octaveChange 1 ''' def __init__(self, **keywords): super().__init__(**keywords) self.octaveChange = 1 self.lowestLine = (7 * 3) + 3
[docs] class GSopranoClef(GClef): ''' A G clef on the middle line, formerly occasionally used for soprano parts. >>> a = clef.GSopranoClef() >>> a.sign 'G' >>> a.line 3 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 3 self.lowestLine = (7 * 4) + 1
# ------------------------------------------------------------------------------
[docs] class CClef(PitchClef): ''' A generic C Clef, with no line set. >>> a = clef.CClef() >>> a.sign 'C' ''' def __init__(self, **keywords): super().__init__(**keywords) self.sign = 'C'
[docs] class SopranoClef(CClef): ''' A soprano clef, with C on the lowest line (found in Bach often). >>> a = clef.SopranoClef() >>> a.sign 'C' >>> a.line 1 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 1 self.lowestLine = (7 * 4) + 1
[docs] class MezzoSopranoClef(CClef): ''' A C clef with C on the second line. Perhaps the rarest of the C clefs. >>> a = clef.MezzoSopranoClef() >>> a.sign 'C' >>> a.line 2 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 2 self.lowestLine = (7 * 3) + 6
[docs] class AltoClef(CClef): ''' A C AltoClef, common for violas. >>> a = clef.AltoClef() >>> a.sign 'C' >>> a.line 3 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 3 self.lowestLine = (7 * 3) + 4
[docs] class TenorClef(CClef): ''' A C Tenor Clef, often used in bassoon and cello parts and orchestral trombone parts. >>> a = clef.TenorClef() >>> a.sign 'C' >>> a.line 4 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 4 self.lowestLine = (7 * 3) + 2
[docs] class CBaritoneClef(CClef): ''' A Baritone C clef (as opposed to an F Baritone Clef). >>> a = clef.CBaritoneClef() >>> a.sign 'C' >>> a.line 5 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 5 self.lowestLine = (7 * 2) + 7
# ------------------------------------------------------------------------------
[docs] class FClef(PitchClef): ''' A generic F-Clef, like a Bass clef. >>> a = clef.FClef() >>> a.sign 'F' ''' def __init__(self, **keywords): super().__init__(**keywords) self.sign = 'F'
[docs] class FBaritoneClef(FClef): ''' An F Baritone Clef. >>> a = clef.FBaritoneClef() >>> a.sign 'F' >>> a.line 3 >>> b = clef.CBaritoneClef() >>> a.lowestLine == b.lowestLine True >>> a.sign == b.sign False ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 3 self.lowestLine = (7 * 2) + 7
[docs] class BassClef(FClef): ''' A standard Bass Clef >>> a = clef.BassClef() >>> a.sign 'F' ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 4 self.lowestLine = (7 * 2) + 5
[docs] class Bass8vbClef(FClef): ''' A bass clef configured to be an octave lower. >>> a = clef.Bass8vbClef() >>> a.sign 'F' >>> a.octaveChange -1 ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 4 self.octaveChange = -1 self.lowestLine = (7 * 2) + 5
[docs] class Bass8vaClef(FClef): ''' A rarely used Bass Clef an octave higher. >>> a = clef.Bass8vaClef() >>> a.sign 'F' ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 4 self.octaveChange = 1 self.lowestLine = (7 * 2) + 5
[docs] class SubBassClef(FClef): ''' An F clef on the top line. >>> a = clef.SubBassClef() >>> a.sign 'F' ''' def __init__(self, **keywords): super().__init__(**keywords) self.line = 5 self.lowestLine = (7 * 2) + 3
# ------------------------------------------------------------------------------ CLASS_FROM_TYPE: dict[str, list[type[Clef]|None]] = { 'G': [None, FrenchViolinClef, TrebleClef, GSopranoClef, None, None], 'C': [None, SopranoClef, MezzoSopranoClef, AltoClef, TenorClef, CBaritoneClef], 'F': [None, None, None, FBaritoneClef, BassClef, SubBassClef], 'TAB': [None, None, None, None, None, TabClef] }
[docs] def clefFromString(clefString, octaveShift=0) -> Clef: ''' Returns a Clef object given a string like "G2" or "F4" etc. Does not refer to a violin/guitar string. >>> tc = clef.clefFromString('G2') >>> tc <music21.clef.TrebleClef> >>> nonStandard1 = clef.clefFromString('F1') >>> nonStandard1 <music21.clef.FClef> >>> nonStandard1.line 1 >>> nonStandard2 = clef.clefFromString('D4') >>> nonStandard2 <music21.clef.PitchClef> >>> nonStandard2.sign 'D' >>> nonStandard2.line 4 >>> tc8vb = clef.clefFromString('G2', -1) >>> tc8vb <music21.clef.Treble8vbClef> Three special clefs, Tab, Percussion, and None are also supported. >>> tabClef = clef.clefFromString('TAB') >>> tabClef <music21.clef.TabClef> Case does not matter. >>> tc8vb = clef.clefFromString('g2', -1) >>> tc8vb <music21.clef.Treble8vbClef> >>> percussionClef = clef.clefFromString('Percussion') >>> percussionClef <music21.clef.PercussionClef> >>> noClef = clef.clefFromString('None') >>> noClef <music21.clef.NoClef> Invalid line numbers raise an exception: >>> invalidClef = clef.clefFromString('F6') Traceback (most recent call last): music21.clef.ClefException: line number (second character) must be 1-5; do not use this function for clefs on special staves such as 'F6' Can find any clef in the module >>> clef.clefFromString('Treble') <music21.clef.TrebleClef> >>> clef.clefFromString('trebleclef') <music21.clef.TrebleClef> >>> clef.clefFromString('treble8vb') <music21.clef.Treble8vbClef> ''' xnStr = clefString.strip() if xnStr.lower() in ('tab', 'percussion', 'none', 'jianpu'): if xnStr.lower() == 'tab': return TabClef() elif xnStr.lower() == 'percussion': return PercussionClef() elif xnStr.lower() == 'none': return NoClef() elif xnStr.lower() == 'jianpu': return JianpuClef() if len(xnStr) == 2: (thisType, lineNum) = (xnStr[0].upper(), int(xnStr[1])) elif len(xnStr) == 1: # some Humdrum files have just ClefG, eg. Haydn op. 9 no 3, mvmt 1 thisType = xnStr[0].upper() if thisType == 'G': lineNum = 2 elif thisType == 'F': lineNum = 4 elif thisType == 'C': lineNum = 3 else: lineNum = False elif len(xnStr) > 2: from music21 import clef as myself xnLower = xnStr.lower() for x in dir(myself): if 'Clef' not in x: continue if xnLower != x.lower() and xnLower + 'clef' != x.lower(): continue objType = getattr(myself, x) if isinstance(objType, type): return objType() raise ClefException('Could not find clef ' + xnStr) else: raise ClefException('Entry has clef info but no clef specified') if octaveShift != 0: params = (thisType, lineNum, octaveShift) if params == ('G', 2, -1): return Treble8vbClef() elif params == ('G', 2, 1): return Treble8vaClef() elif params == ('F', 4, -1): return Bass8vbClef() elif params == ('F', 4, 1): return Bass8vaClef() # other octaveShifts will pass through if thisType is False or lineNum is False: raise ClefException(f'cannot read {xnStr} as clef str, should be G2, F4, etc.') if lineNum < 1 or lineNum > 5: raise ClefException('line number (second character) must be 1-5; do not use this ' f'function for clefs on special staves such as {xnStr!r}') clefObj: Clef if thisType in CLASS_FROM_TYPE: line_list = CLASS_FROM_TYPE[thisType] assert isinstance(line_list, list) if line_list[lineNum] is None: if thisType == 'G': clefObj = GClef() elif thisType == 'F': clefObj = FClef() elif thisType == 'C': clefObj = CClef() elif thisType == 'TAB': clefObj = TabClef() else: # pragma: no cover clefObj = PitchClef() clefObj.line = lineNum else: ClefType = line_list[lineNum] if t.TYPE_CHECKING: assert ClefType is not None assert issubclass(ClefType, PitchClef) clefObj = ClefType() else: clefObj = PitchClef() clefObj.sign = thisType clefObj.line = lineNum if octaveShift != 0: clefObj.octaveChange = octaveShift return clefObj
[docs] def bestClef(streamObj: stream.Stream, allowTreble8vb=False, recurse=False) -> PitchClef: # noinspection PyShadowingNames ''' Returns the clef that is the best fit for notes and chords found in this Stream. >>> import random >>> a = stream.Stream() >>> for x in range(30): ... n = note.Note() ... n.pitch.midi = random.randint(70, 81) ... a.insert(n) >>> b = clef.bestClef(a) >>> b <music21.clef.TrebleClef> >>> b.line 2 >>> b.sign 'G' >>> c = stream.Stream() >>> for x in range(10): ... n = note.Note() ... n.pitch.midi = random.randint(45, 54) ... c.insert(n) >>> d = clef.bestClef(c) >>> d <music21.clef.BassClef> >>> d.line 4 >>> d.sign 'F' This does not automatically get a flat representation of the Stream. There are a lot more high notes in `a` (30) than low notes in `c` (10), but it will not matter here, because the pitches in `a` will not be found: >>> c.insert(0, a) >>> clef.bestClef(c) <music21.clef.BassClef> But with recursion, it will matter: >>> clef.bestClef(c, recurse=True) <music21.clef.TrebleClef> Notes around middle C can get Treble8vb if the setting is allowed: >>> clef.bestClef(stream.Stream([note.Note('D4')])) <music21.clef.TrebleClef> >>> clef.bestClef(stream.Stream([note.Note('D4')]), allowTreble8vb=True) <music21.clef.Treble8vbClef> Streams of extremely high notes or extremely low notes can get Treble8va or Bass8vb clefs: >>> clef.bestClef(stream.Stream([note.Note('D7')])) <music21.clef.Treble8vaClef> >>> clef.bestClef(stream.Stream([note.Note('C0')])) <music21.clef.Bass8vbClef> ''' def findHeight(pInner): height = pInner.diatonicNoteNum if pInner.diatonicNoteNum > 33: # a4 height += 3 # bonus elif pInner.diatonicNoteNum < 24: # Bass F or lower height += -3 # bonus return height # environLocal.printDebug(['calling bestClef()']) totalPitches = 0 totalHeight = 0 sIter = streamObj.recurse() if recurse else streamObj.iter() notes = sIter.notesAndRests for n in notes: for p in n.pitches: totalPitches += 1 totalHeight += findHeight(p) if totalPitches == 0: averageHeight = 29.0 else: averageHeight = totalHeight / totalPitches # environLocal.printDebug(['average height', averageHeight]) if averageHeight > 49: # value found with experimentation; revise return Treble8vaClef() elif allowTreble8vb and averageHeight > 32: return TrebleClef() elif not allowTreble8vb and averageHeight > 28: # c4 return TrebleClef() elif allowTreble8vb and averageHeight > 26: return Treble8vbClef() elif averageHeight > 10: # value found with experimentation; revise return BassClef() else: return Bass8vbClef()
# ------------------------------------------------------------------------------ # all other tests in test/test_clef class Test(unittest.TestCase): def testCopyAndDeepcopy(self): from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) # ------------------------------------------------------------------------------ # define presented order in documentation _DOC_ORDER = [Clef, TrebleClef, BassClef] if __name__ == '__main__': import music21 music21.mainTest(Test)