Source code for maelzel.scorestruct

from __future__ import annotations

from pathlib import Path
from dataclasses import dataclass
from bisect import bisect
import functools
import re

import emlib.textlib
from maelzel.common import F, asF, F0

from typing import TYPE_CHECKING, overload as _overload
if TYPE_CHECKING:
    from typing import Iterator, Sequence, Iterable, Any
    from typing_extensions import Self
    import maelzel.core
    from maelzel.common import num_t, timesig_t, beat_t
    from maelzel.scoring.renderoptions import RenderOptions
    from maelzel.scoring.renderer import Renderer


__all__ = (
    'asF',
    'ScoreStruct',
    'MeasureDef',
    'measureBeatStructure',
    'TimeSignature'
)


def _partialsum(seq: Iterable[F], start=F0) -> list[F]:
    """
    for each elem in seq return the partial sum

    .. code::

        n0 -> n0
        n1 -> n0 + n1
        n2 -> n0 + n1 + n2
        n3 -> n0 + n1 + n2 + n3
    """
    accum = start
    out = []
    for i in seq:
        accum += i
        out.append(accum)
    return out


@dataclass
class TimeInterval:
    start: F
    end: F

    @property
    def duration(self) -> F:
        return self.end - self.start

    def __iter__(self):
        return iter((self.start, self.end))

    def __getitem__(self, idx: int):
        return (self.start, self.end)[idx]

    def __len__(self):
        return 2


@functools.cache
def beatWeightsByTimeSignature(num: int, den: int) -> tuple[int, ...]:
    """
    Given a time signature, returns a sequence of beat weights

    A beat weight is a number from 0 to 2, where 0 means unweighted,
    1 means weak weight and 2 means strong weight. For example, for
    a 4/4 measure the returned weights are (2, 0, 1, 0), where the
    first beat is strong, the second unweighted, the 3rd has a weak
    weight and the 4th unweighted again.

    Args:
        num: numerator of the time signature
        den: denominator ot the time signature

    Returns:
        a sequence of weights (a value 0, 1, or 2) as a tuple

    """
    if num % 2 == 0:
        # 2, 0, 1, 0, 1, 0, ...
        weights = [1, 0] * (num//2)
        weights[0] = 2
        return tuple(weights)
    elif num % 3 == 0:
        weights = [1, 0, 0] * (num//3)
        weights[0] = 2
        return tuple(weights)
    elif num % 5 == 0:
        return tuple([2, 0, 1, 0, 0] * (num//5))
    else:
        # All uneven signatures: binary pairs and the last ternary
        weights = [1, 0] * (num//2)
        weights.append(0)
        weights[0] = 2
        return tuple(weights)


[docs] class TimeSignature: """ A time signature In its simplest form a type signature consists of one part, a tuple (numerator, denominator). For example, a 4/4 time signature can be represented as TimeSignature((4, 4)) Args: parts: the parts of this time signature, a seq. of tuples (numerator, denominator) subdivisions: subdivisions as multiples of the denominator. Only valid for non-compound signatures. It is used to structure subdivisions for a single part. For example, 7/8 subdivided as 2+3+2 can be expressed as TimeSignature((7, 8), subdivisionStruct=(2, 3, 2)). """ def __init__(self, *parts: tuple[int, int], subdivisions: Sequence[int] = ()): if not all(isinstance(part, tuple) and len(part) == 2 for part in parts): raise ValueError(f"Invalid parts: {parts}") self.parts: tuple[tuple[int, int], ...] = parts """ The parts of this timesig, as originally passed at creation """ minden = max(den for num, den in parts) numerators = [num * minden // den for num, den in parts] self.normalizedParts: tuple[tuple[int, int], ...] = tuple((num, minden) for num in numerators) """ The normalized parts with a shared common denominator parts: (3/4 3/8), common den: 8, normalizedParts: (6/8, 3/8), fusedSignature: 9/8 """ self.fusedSignature: tuple[int, int] = (int(sum(numerators)), minden) """ One signature epresenting all compound parts The fused signature is based on the min. common multiple of the compound parts. For example, a signature 3/4+3/16 will have a fused signature of 15/16 (3*4+3). For non-compound signatures, the fused signature is the same as the time signature itself.""" self.subdivisionStruct: tuple[int, ...] = tuple(subdivisions) """ Subdivisions as multiples of the fused denominator. """
[docs] def copy(self) -> TimeSignature: return TimeSignature(*self.parts, subdivisions=self.subdivisionStruct)
def __hash__(self) -> int: return hash((self.parts, self.subdivisionStruct)) def __eq__(self, other: TimeSignature): return isinstance(other, TimeSignature) and self.parts == other.parts and self.subdivisionStruct == other.subdivisionStruct @property def numerator(self) -> int: """The numerator of this time signature For non-compound signatures, this is the same as the numerator of its only part. For compound signatures, this corresponds to the nominator of the fused signature >>> TimeSignature((7, 8)).numerator 7 >>> TimeSignature((2, 4), (5, 8)).numerator 9 """ return self.fusedSignature[0] @property def denominator(self) -> int: """The denominator of this time signature For non-compound signatures, this is the same as the denominator of its only part. For compound signatures, this corresponds to the denominator of the fused signature >>> TimeSignature((7, 8)).denominator 8 >>> TimeSignature((2, 4), (5, 8)).denominator 8 """ return self.fusedSignature[1] @property def quarternoteDuration(self) -> F: """The duration of this time signature, in quarternotes""" num, den = self.fusedSignature return F(num, den) * 4 def __str__(self): parts = [f"{num}/{den}" for num, den in self.parts] return "+".join(parts) def _reprInfo(self) -> str: if len(self.parts) == 1: num, den = self.parts[0] if self.subdivisionStruct: subdiv = '-'.join(map(str, self.subdivisionStruct)) return f"{num}/{den}({subdiv})" return f"{num}/{den}" elif all(den == self.parts[0][1] for num, den in self.parts): nums = "+".join(str(p[0]) for p in self.parts) return f"{nums}/{self.parts[0][1]}" else: return '+'.join(f"{n}/{d}" for n, d in self.parts) def __repr__(self): return f"TimeSignature({self._reprInfo()})"
[docs] @classmethod def parse(cls, timesig: str | tuple, subdivisionStruct: Sequence[int] = () ) -> TimeSignature: """ Parse a time signature definition Args: timesig: a time signature as a string. For compound signatures, use a + sign between parts. subdivisionStruct: the subdivision structure as multiples of the fused signature denominator. Returns: the time signature """ if isinstance(timesig, tuple): if all(isinstance(_, tuple) for _ in timesig): # ((3, 8), (3, 8), (2, 8)) return TimeSignature(*timesig) elif len(timesig) == 2 and isinstance(timesig[1], int): num, den = timesig if isinstance(num, tuple): # ((3, 3, 2), 8) parts = [(_, den) for _ in num] return TimeSignature(*parts, subdivisions=subdivisionStruct) else: assert isinstance(num, int) return TimeSignature((num, den), subdivisions=subdivisionStruct) else: raise ValueError(f"Cannot parse timesignature: {timesig}") elif isinstance(timesig, str): # Possible signatures: 3/4, 3/8+3/8+2/8, 5/8(3-2), 5/8(3-2)+3/16 parts = timesig.split("+") parsedParts = [_parseTimesigPart(part) for part in parts] if len(parsedParts) == 1: signature, subdivs = parsedParts[0] if subdivs and subdivisionStruct: raise ValueError("Duplicate subdivision structure") return TimeSignature(signature, subdivisions=subdivs or subdivisionStruct) signatures, subdivs = zip(*parsedParts) # We ignore subdivisions for compound signatures return TimeSignature(*signatures, subdivisions=subdivisionStruct) else: raise TypeError(f"Expected a str or a tuple, got {timesig}")
[docs] def isHeterogeneous(self) -> bool: """ Is this a compound meter with heterogeneous denominators? Heterogeneous meters are 3/4+3/8, 4/4+1/8, 3/8+3/16. Homogeneous meters are 3/8+2/8, 3/4+4/4 Returns: true if this is a compound meter with heterogenous denominators """ if len(self.parts) == 1: return False denoms = set(denom for num, denom in self.parts) return len(denoms) >= 2
[docs] def qualifiedSubdivisionStruct(self) -> tuple[int, tuple[int, ...]]: """ The qualified subdivision structure, a tuple (denominator, subdivisions) where the denominator is the max. common denom. of the parts of this signature and the subdivisions are the subdivisions as given in the subdivisionStruct This method will raise ValueError if this time signature does not have a subdivision structure Example ~~~~~~~ >>> t = TimeSignature((7, 8), subdivisionStruct=(3, 2, 2)) >>> t.qualifiedSubdivisionStruct() (8, (3, 2, 2)) >>> t2 = TimeSignature((2, 4), (3, 16), subdivisionStruct=(4, 4, 3)) >>> t2.fusedSignature (11, 16) >>> t2.qualifiedSubdivisionStruct() (16, (4, 4, 3)) """ if not self.subdivisionStruct: raise ValueError("This time signature does not have a subdivision structure") return self.fusedSignature[1], self.subdivisionStruct
def _parseTimesigPart(s: str) -> tuple[tuple[int, int], tuple[int, ...]]: """ Given a string in the form 5/8(3-2), returns ((5, 8), (3, 2)) For 5/8, returns ((5, 8), ()) """ if "(" in s: assert s.count("(") == 1 and s[-1] == ")", f"Invalid time signature part: {s}" p1, p2 = s[:-1].split("(") nums, dens = p1.split("/") num, den = int(nums), int(dens) subdivs = tuple(int(subdiv) for subdiv in p2.split("-")) return ((num, den), subdivs) else: fracparts = s.split("/") if len(fracparts) != 2: raise ValueError(f"Invalid time signature: {s}") nums, dens = fracparts return ((int(nums), int(dens)), ()) def _parseTimesig(s: str) -> tuple[int, int]: try: num, den = s.split("/") except ValueError: raise ValueError(f"Could not parse timesig: {s}") if "+" in num: parts = num.split("+") # Compound timesigs are not supported, we just add num = sum(int(p) for p in parts) return num, int(den) return int(num), int(den) def _asTimeSignature(timesig: str | timesig_t | TimeSignature ) -> TimeSignature: if isinstance(timesig, TimeSignature): return timesig elif isinstance(timesig, str): return TimeSignature.parse(timesig) elif isinstance(timesig, tuple): return TimeSignature(timesig) else: raise TypeError(f"Cannot convert {timesig} to a TimeSignature") # def _asTimesig(t: str | timesig_t) -> timesig_t: # if isinstance(t, tuple): # assert len(t) == 2 # return t # elif isinstance(t, str): # return _parseTimesig(t) # else: # raise TypeError(f"Expected a tuple (5, 8) or a string '5/8', got {t}, {type(t)}") @dataclass class _ScoreLine: measureIndex: int | None timesig: TimeSignature | None tempo: float | None label: str = '' barline: str = '' rehearsalMark: str = '' @dataclass class RehearsalMark: text: str box: str = '' def __post_init__(self): assert self.text @dataclass class TempoDef: tempo: F refvalue: int = 4 numdots: int = 0 @property def quartertempo(self) -> F: return asQuarterTempo(self.tempo, refvalue=self.refvalue, numdots=self.numdots) @dataclass class KeySignature: fifths: int mode: str = 'major' def __post_init__(self): assert isinstance(self.fifths, int) and -7 <= self.fifths <= 7 assert self.mode in ('', 'major', 'minor'), f"Invalid mode: {self.mode}" @functools.cache def asQuarterTempo(tempo: F, refvalue: int, numdots: int = 0) -> F: refdur = F(4, refvalue) if numdots > 0: den = 2 ** numdots num = (2 ** (numdots + 1)) - 1 refdur = refdur * num / den qtempo = tempo * refdur return qtempo def _parseTempoRefvalue(ref: str) -> tuple[int, int]: if "." not in ref: refvaluestr = ref numdots = 0 else: try: refvaluestr, dots = ref.split(".", maxsplit=1) numdots = len(dots) + 1 except ValueError as e: raise ValueError(f"Could not parse tempo: '{ref}'") from e try: refvalue = int(refvaluestr) except ValueError as e: raise ValueError(f"Could not parse tempo '{ref}', invalid reference value '{refvaluestr}'") from e if refvalue not in (1, 2, 4, 8, 16, 32, 64): raise ValueError(f"Could not parse tempo: '{ref}', reference value {refvalue} should be a power of 2") return refvalue, numdots def _parseTempo(s: str) -> TempoDef: """ Parse a tempo ======= ===== ========== ======= ============== Value tempo refvalue numdots quartertempo ======= ===== ========== ======= ============== 60 60 4 0 60 4=60 60 4 0 60 8=60 60 8 0 120 4.=60 60 4 1 90 4..=60 60 4 2 112.5 ======= ===== ========== ======= ============== Args: s: the string to parse Returns: A TempoDef .. code-block:: python >>> _parseTempo('8=60').quartertempo 120 >>> _parseTempo('4.=60').quartertempo 90 """ if "=" not in s: try: tempo = F(s) return TempoDef(tempo, 4, 0) except ValueError as e: raise ValueError(f"Could not parse tempo: {s}") from e parts = [_.strip() for _ in s.split("=")] if len(parts) != 2: raise ValueError(f"Could not parse tempo: {s}") ref, tempostr = parts tempo = float(tempostr) refvalue, numdots = _parseTempoRefvalue(ref) return TempoDef(F(tempo), refvalue, numdots) def _parseScoreStructLine(line: str) -> _ScoreLine: """ parse a line of a ScoreStruct definition The line has the format ``[measureIndex, ] timesig [, tempo] [keywords] * timesig has the format ``num/den`` * keywords have the format ``keyword=value``, where keyword can be one of ``rehearsalmark``, ``label`` and ``barline`` (case is not important). *rehearsalmark* adds a rehearsal mark, *label* adds a text label and *barline* customizes the right barline (possible values: 'single', 'double', 'solid', 'dotted' or 'dashed' Args: line: a line of the format [measureIndex, ] timesig [, tempo] Returns: a tuple (measureIndex, timesig, tempo), where only timesig is required """ line = line.strip() args = [] keywords = {} tempodef: TempoDef | None = None for part in [_.strip() for _ in line.split(",")]: if '=' not in part: args.append(part) else: key, value = part.split('=', maxsplit=1) key = key.strip() if re.match(r"(1|2|4|8|16|32|64)\.*", key): refvalue, numdots = _parseTempoRefvalue(key) tempodef = TempoDef(tempo=F(value), refvalue=refvalue, numdots=numdots) else: value = value.strip() if value[0] == value[-1] in "'\"": value = value[1:-1] keywords[key] = value numargs = len(args) label = '' barline = '' rehearsalmark = '' measure: int | None = None if numargs == 1: arg = args[0] if "/" in arg: timesigS = arg elif tempodef is None: try: tempodef = _parseTempo(arg) timesigS = '' except ValueError: raise ValueError(f"Could not parse line argument: '{arg}'") else: raise ValueError(f"Could not parse line argument: '{arg}'") elif numargs == 2: if "/" in args[0]: timesigS, tempoS = args measure = None try: _tempodef = _parseTempo(tempoS) if tempodef is not None: raise ValueError(f"Multiple tempos: {tempoS} and {tempodef}") tempodef = _tempodef except ValueError: raise ValueError(f"Could not parse the tempo ({tempoS}) as a number (line: {line})") else: measureIndexS, timesigS = args try: measure = int(measureIndexS) except ValueError: raise ValueError(f"Could not parse the measure index '{measureIndexS}' while parsing line: '{line}'") elif numargs == 3: if "/" not in args[0]: measureIndexS, timesigS, tempoS = [_.strip() for _ in args] measure = int(measureIndexS) if measureIndexS else None else: measure = None timesigS, tempoS, label = [_.strip() for _ in args] if tempoS: if tempodef is not None: raise ValueError(f"Multiple tempos: {tempoS} and {tempodef}") tempodef = _parseTempo(tempoS) elif numargs == 4: measureIndexS, timesigS, tempoS, label = [_.strip() for _ in args] measure = int(measureIndexS) if measureIndexS else None if tempoS: tempodef = _parseTempo(tempoS) else: raise ValueError(f"Parsing error at line {line}") timesig = TimeSignature.parse(timesigS) if timesigS else None for k, v in keywords.items(): k = k.lower() if k == 'label': label = v elif k == 'barline': assert v in _barstyles, f"Expected a barline style ({_barstyles}), got {v}" barline = v elif k == 'rehearsalmark': rehearsalmark = v else: raise ValueError(f"Key {k} unknown (value: {v}) while reading score line {line}") if label: label = label.replace('"', '') return _ScoreLine(measureIndex=measure, timesig=timesig, tempo=tempodef.quartertempo if tempodef else None, label=label, barline=barline, rehearsalMark=rehearsalmark) _barstyles = {'single', 'final', 'double', 'solid', 'dotted', 'dashed', 'tick', 'short', 'double-thin', 'none'}
[docs] class MeasureDef: """ A MeasureDef defines one Measure within a ScoreStruct (time signature, tempo, etc.) """ __slots__ = ( '_timesig', '_quarterTempo', '_barline', 'annotation', 'timesigInherited', 'tempoInherited', 'rehearsalMark', 'keySignature', 'properties', 'parent', 'readonly', 'durationQuarters', '_subdivisionTempoThreshold', '_beatWeightTempoThreshold', '_durationSecs' ) def __init__(self, timesig: TimeSignature, quarterTempo: F | int | float, parent: ScoreStruct | None = None, annotation='', timesigInherited=False, tempoInherited=False, barline='', rehearsalMark: RehearsalMark | str = '', keySignature: KeySignature | None = None, properties: dict | None = None, subdivTempoThresh: int | None = None, beatWeightTempoThresh: int | None = None, readonly=True ): if barline and barline not in _barstyles: raise ValueError(f"Unknown barline style: '{barline}', possible values: {_barstyles}") if isinstance(rehearsalMark, str): rehearsalMark = RehearsalMark(rehearsalMark) assert isinstance(timesig, TimeSignature), f"Expected a TimeSignature, got {timesig}" self._timesig: TimeSignature = timesig self._quarterTempo = asF(quarterTempo) self.annotation = annotation """Any text annotation for this measure""" self.timesigInherited = timesigInherited """Is the time-signature of this measure inherited?""" self.tempoInherited = tempoInherited """Is the tempo of this measure inherited?""" self._barline = barline """The barline style, or '' to use default""" self.rehearsalMark: RehearsalMark | None = rehearsalMark or None """If given, a RehearsalMark for this measure""" self.keySignature = keySignature """If given, a key signature""" self.properties = properties """User defined properties can be placed here. None by default""" self.parent = parent """The parent ScoreStruct of this measure, if any""" self.readonly = readonly """Is this measure definition read only?""" self._subdivisionTempoThreshold: int | None = subdivTempoThresh """The max. tempo at which an eighth note can be a beat of its own""" self._beatWeightTempoThreshold: int | None = beatWeightTempoThresh self.durationQuarters: F = self.timesig.quarternoteDuration self._durationSecs: F = F0 @property def durationSecs(self) -> F: """The duration of this measure in seconds""" if not (dur := self._durationSecs): self._durationSecs = dur = self.durationQuarters * (F(60) / self._quarterTempo) return dur @property def timesig(self) -> TimeSignature: """The time signature of this measure. Can be explicit or inherited""" return self._timesig @timesig.setter def timesig(self, timesig): if self.readonly: raise ValueError("This MeasureDef is readonly") self._timesig = TimeSignature.parse(timesig) self.timesigInherited = False if self.parent: self.parent.modified() @property def quarterTempo(self) -> F: """The tempo relative to a quarternote""" return self._quarterTempo @quarterTempo.setter def quarterTempo(self, tempo: F | int): if self.readonly: raise ValueError("This MeasureDef is readonly") self._quarterTempo = asF(tempo) self.tempoInherited = False if self.parent: self.parent.modified() @property def barline(self) -> str: """The barline style, or '' to use default""" return self._barline @barline.setter def barline(self, linestyle: str): if self.readonly: raise ValueError("This MeasureDef is readonly") if linestyle not in _barstyles: raise ValueError(f'Unknown barstyle: {linestyle}, possible values: {_barstyles}') self._barline = linestyle
[docs] def subdivTempoThresh(self, fallback=96) -> int: if self._subdivisionTempoThreshold: return self._subdivisionTempoThreshold elif self.parent and self.parent.subdivTempoThresh: return self.parent.subdivTempoThresh assert isinstance(fallback, int) return fallback
[docs] def beatWeightTempoThresh(self, fallback=52) -> int: assert isinstance(fallback, (int, float, F)) if self._beatWeightTempoThreshold: return self._beatWeightTempoThreshold if self.parent: return self.parent.beatWeightTempoThresh or fallback return fallback
[docs] def beatStructure(self) -> list[BeatDef]: """ Beat structure of this measure Returns: a list of tuple with the form (beatOffset: F, beatDur: F, beatWeight: int) for each beat of this measure """ return measureBeatStructure(self.timesig, quarterTempo=self.quarterTempo, subdivisionStructure=self.subdivisionStructure(), subdivTempoThresh=self.subdivTempoThresh(), beatWeightTempoThresh=self.beatWeightTempoThresh())
[docs] def asScoreLine(self) -> str: """ The representation of this MeasureDef as a score line """ num = self.timesig.numerator den = self.timesig.denominator parts = [f'{num}/{den}, {self.quarterTempo}'] if self.annotation: parts.append(self.annotation) return ', '.join(parts)
def __copy__(self): return MeasureDef(timesig=self._timesig, quarterTempo=self._quarterTempo, annotation=self.annotation, timesigInherited=self.timesigInherited, tempoInherited=self.tempoInherited, keySignature=self.keySignature, rehearsalMark=self.rehearsalMark, barline=self.barline, readonly=self.readonly, parent=self.parent, beatWeightTempoThresh=self._beatWeightTempoThreshold, subdivTempoThresh=self._subdivisionTempoThreshold)
[docs] def copy(self) -> MeasureDef: return self.__copy__()
def __repr__(self): parts = [f'timesig={self._timesig}, quarterTempo={self._quarterTempo}'] if self.annotation: parts.append(f'annotation="{self.annotation}"') if self.timesigInherited: parts.append('timesigInherited=True') if self.tempoInherited: parts.append('tempoInherited=True') if self.barline: parts.append(f'barline={self.barline}') if self.keySignature: parts.append(f'keySignature={self.keySignature}') if self.rehearsalMark: parts.append(f'rehearsalMark={self.rehearsalMark}') if self.readonly: parts.append('readonly=True') return f'MeasureDef({", ".join(parts)})' def __hash__(self) -> int: return hash((self.timesig, self.quarterTempo, self.annotation))
[docs] def subdivisionStructure(self) -> tuple[int, tuple[int, ...]]: """ Max. common denominator for subdivisions and the subdivisions as multiples of it For example, for 3/4+3/8, returns (8, (2, 2, 2, 3)) Returns: a tuple (max. common denominator, subdivisions as multiples of common denominator) """ subdivs = self.subdivisions() denom = self.timesig.fusedSignature[1] if denom == 2: denom = max(subdiv.denominator for subdiv in subdivs) * 4 multiples = tuple((subdiv * denom // 4) for subdiv in subdivs) assert denom in (1, 2, 4, 8, 16, 32, 64, 128, 256) assert all(n >= 1 for n in multiples), f"Invalid subdivision structure: {denom=}, {multiples=}, {subdivs=} in {self}" return denom, multiples
[docs] def subdivisions(self) -> list[F]: """ Returns a list of the durations representing the subdivisions of this measure. A subdivision is a duration, in quarters. Returns: a list of durations which sum up to the duration of this measure Example ------- >>> MeasureDef(timesig=TimeSignature((3, 4)), quarterTempo=60).subdivisions() [1, 1, 1] >>> MeasureDef(timesig=TimeSignature((3, 8)), quarterTempo=60).subdivisions() [0.5, 0.5, 0.5] >>> MeasureDef(timesig=TimeSignature((7, 8)), quarterTempo=40).subdivisions() [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5] >>> MeasureDef(timesig=TimeSignature((7, 8)), quarterTempo=150).subdivisions() [1.0, 1.0, 1.5] >>> MeasureDef(TimeSignature((7, 8), subdivisionStruct=(2, 3, 2)), quarterTempo=150).subdivisions() [1, 1.5, 1] """ return measureSubdivisions(timesig=self.timesig, quarterTempo=self.quarterTempo, subdivTempoThresh=self.subdivTempoThresh(), )
[docs] def timesigRepr(self) -> str: """ Returns a string representation of this measure's time signature Returns: a string representation of this measure's time-signature """ return self.timesig._reprInfo()
# # parts = [f"{num}/{den}" for num, den in self.timesig.parts] # partstr = "+".join(parts) # if self.timesig.subdivisionStruct: # subdivs = "+".join(_subdivRepr(f, self.timesig.denominator) for f in self.subdivisions()) # partstr += f"({subdivs})" # return partstr def _subdivRepr(f: F, timesigDen: int) -> str: f = f * (timesigDen // 4) if f.denominator == 1: return str(f.numerator) return f"{f.numerator}/{f.denominator}" def inferSubdivisions(num: int, den: int, quarterTempo: F ) -> tuple[int, ...]: if (den == 8 or den == 16) and num % 3 == 0: return tuple([3] * (num // 3)) subdivs = [] while num > 3: subdivs.append(2) num -= 2 if num: subdivs.append(num) return tuple(subdivs) def measureQuarterDuration(timesig: timesig_t) -> F: """ The duration in quarter notes of a measure according to its time signature Args: timesig: a tuple (num, den) Returns: the duration in quarter notes Examples:: >>> measureQuarterDuration((3,4)) 3 >>> measureQuarterDuration((5, 8)) 2.5 """ num, den = timesig quarterDuration = F(num)/den * 4 return quarterDuration def _checkSubdivisionStructure(s: tuple[int, tuple[int, ...]]) -> None: assert isinstance(s, tuple) and len(s) == 2, s assert isinstance(s[0], int) and isinstance(s[1], tuple), s assert all(isinstance(div, int) for div in s[1]), s def measureSubdivisions(timesig: TimeSignature, quarterTempo: F, subdivisionStructure: tuple[int, tuple[int, ...]] = (), subdivTempoThresh: int = 96 ) -> list[F]: if subdivisionStructure: div, nums = subdivisionStructure assert all(n >= 1 for n in nums), f"Invalud {subdivisionStructure=}" if len(timesig.parts) == 1: if subdivisionStructure and timesig.subdivisionStruct: subdivisionStructure = timesig.qualifiedSubdivisionStruct() return beatDurations(timesig=timesig.parts[0], quarterTempo=quarterTempo, subdivisionStructure=subdivisionStructure, subdivTempoThresh=subdivTempoThresh) subdivs = [] for part in timesig.parts: # TODO: use the subdivision structure in the timesig, if present beatdurs = beatDurations(timesig=part, quarterTempo=quarterTempo, subdivTempoThresh=subdivTempoThresh) subdivs.extend(beatdurs) return subdivs def beatDurations(timesig: timesig_t, quarterTempo: F, subdivTempoThresh: int = 96, subdivisionStructure: tuple[int, tuple[int, ...]] = () ) -> list[F]: """ Returns the beat durations for the given time signature Args: timesig: the timesignature of the measure or of the part of the measure quarterTempo: the tempo for a quarter note subdivTempoThresh: max quarter tempo to divide a measure like 5/8 in all eighth notes instead of, for example, 2+2+1 subdivisionStructure: if given, a tuple (denominator, list of subdivision lengths) For example, a 5/8 measure could have a subdivision structure of (8, (2, 3)) or (8, (3, 2)). Returns: a list of durations, as Fraction :: 4/8 -> [1, 1] 2/4 -> [1, 1] 3/4 -> [1, 1, 1] 5/8 -> [1, 1, 0.5] 5/16 -> [0.5, 0.5, 0.25] """ assert isinstance(subdivTempoThresh, (int, float, F)), f"Invalid {subdivTempoThresh=}" quarterTempo = asF(quarterTempo) quarters = measureQuarterDuration(timesig) num, den = timesig if subdivisionStructure: _checkSubdivisionStructure(subdivisionStructure) subdivden, subdivnums = subdivisionStructure assert subdivden in (1, 2, 4, 8, 16, 32, 64, 128), f"Invalid {subdivisionStructure=}" assert all(num >= 1 for num in subdivnums), f"Invalid {subdivisionStructure=}" quarterden = F(subdivden, 4) if quarterden < 1: numfactor = 1/quarterden quarterden = 1 else: numfactor = 1 quarterden = int(quarterden) subdivisions = [F(num*numfactor, quarterden) for num in subdivnums] if sum(subdivisions) != quarters: raise ValueError(f"The sum of the subdivisions ({sum(subdivisions)}) does not" f"match the number of quarters ({quarters}) in this time " f"signature ({timesig[0]}/{timesig[1]}). Subdivision structure: " f"{subdivisionStructure}") return subdivisions elif den == 4 or den == 2: return [F(1)] * quarters.numerator elif den == 8: if quarterTempo <= subdivTempoThresh/2: # render all beats as 1/8 notes return [F(1, 2)]*num subdivstruct = inferSubdivisions(num=num, den=den, quarterTempo=quarterTempo) return [F(num, den // 4) for num in subdivstruct] elif den == 16: beatdurs = beatDurations((num, 8), quarterTempo=quarterTempo*2) return [dur/2 for dur in beatdurs] elif den == 32: beatdurs = beatDurations((num, 8), quarterTempo=quarterTempo * 4) return [dur/4 for dur in beatdurs] else: raise ValueError(f"Invalid time signature: {timesig}") @dataclass class BeatDef: offset: F duration: F weight: int = 0 def isBinary(self) -> bool: return self.duration.numerator != 3 @property def end(self) -> F: return self.offset + self.duration
[docs] @functools.cache def measureBeatStructure(timesig: TimeSignature | tuple[int, int], quarterTempo: F, subdivisionStructure: tuple[int, tuple[int, ...]] = (), beatWeightTempoThresh = 52, subdivTempoThresh = 96 ) -> list[BeatDef]: """ Returns the beat structure for this measure Args: timesig: the time signature quarterTempo: the tempo of the quarter note subdivisionStructure: the subdivision structure in the form (denominator: int, subdivisions). For example a 7/8 bar divided in 3+2+2 would have a subdivision strucutre of (8, (3, 2, 2)). A 4/4 measure divided in 3/8+3/8+2/8+2/8 would be (8, (3, 3, 2, 2)) beatWeightTempoThresh: a beat resulting in a tempo higher than this is by default assigned a weak weight. This means that beats with a tempo slower than this are always considered strong beats, indicating that beams and syncopations across these beats should be broken subdivTempoThresh: a regular subdivision of a beat resulting in a tempo lower than this can be promoted to a beat of its own. For example, with a quarterTempo of 44, a 5/8 measure would be seen as 5 beats, each of 1/8 note length. For a faster tempo, this would result in a beat of 2/8 and a ternary beat of 5/8 Returns: a list of (beat offset: F, beat duration: F, beat weight: int) """ assert isinstance(beatWeightTempoThresh, (int, float, F)) assert isinstance(subdivTempoThresh, (int, float, F)) if isinstance(timesig, tuple): timesig = TimeSignature(timesig) durations = measureSubdivisions(timesig=timesig, quarterTempo=quarterTempo, subdivisionStructure=subdivisionStructure, subdivTempoThresh=subdivTempoThresh) N = len(durations) if N == 1: weights = [1] elif N % 2 == 0: weights = [1, 0] * (N//2) elif N % 3 == 0: weights = [1, 0, 0] * (N//3) else: weights = [1, 0] * (N//2) weights.append(0) now = F(0) beatOffsets = [] weakBeatDurThreshold = F(60) / beatWeightTempoThresh for i, dur in enumerate(durations): beatOffsets.append(now) now += dur if dur.numerator == 3: weights[i] = 1 else: beatRealDur = dur * (F(60)/quarterTempo) if beatRealDur > weakBeatDurThreshold: weights[i] += 1 # weights[0] = max(weights) + 1 assert len(beatOffsets) == len(durations) == len(weights) return [BeatDef(offset, duration, weight) for offset, duration, weight in zip(beatOffsets, durations, weights)]
def measureBeatOffsets(timesig: timesig_t, quarterTempo: F | int, subdivisionStructure: tuple[int, ...] = () ) -> list[F]: """ Returns a list with the offsets of all beats in measure. The last value refers to the offset of the end of the measure Args: timesig: the timesignature as a tuple (num, den) quarterTempo: the tempo correponding to a quarter note subdivisionStructure: if given, a list of subdivision lengths. For example, a 5/8 measure could have a subdivision structure of (2, 3) or (3, 2) Returns: a list of fractions representing the start time of each beat, plus the end time of the measure (== the start time of the next measure) Example:: >>> measureBeatOffsets((5, 8), 60) [Fraction(0, 1), Fraction(1, 1), Fraction(2, 1), Fraction(5, 2)] # 0, 1, 2, 2.5 """ quarterTempo = asF(quarterTempo) subdivstruct = () if not subdivisionStructure else (timesig[1], subdivisionStructure) beatdurs = beatDurations(timesig, quarterTempo=quarterTempo, subdivisionStructure=subdivstruct) beatOffsets = [F(0)] beatOffsets += _partialsum(beatdurs) return beatOffsets
[docs] class ScoreStruct: """ A ScoreStruct holds the structure of a score but no content A ScoreStruct consists of some metadata and a list of :class:`MeasureDefs`, where each :class:`MeasureDef` defines the properties of the measure at the given index. If a ScoreStruct is marked as *endless*, it is possible to query it (convert beats to time, etc.) outside the defined measures. The ScoreStruct class is used extensively within :py:mod:`maelzel.core` (see `scorestruct-and-maelzel-core`) .. note:: Within the context of maelzel.core, a ScoreStruct needs to be activated to be used implicitely in the current Workspace. To activate a ScoreStruct call its :meth:`~ScoreStruct.activate` method: ``struct = ScoreStruct(...).activate()`` Args: score: if given, a score definition as a string (see below for the format), or a time signature tempo: initial quarter-note tempo. Even if using a time-signature with a smaller denominator (like 3/8), the tempo is always given in reference to a quarter note. endless: mark this ScoreStruct as endless. Defaults to True title: title metadata for the score, used when rendering composer: composer metadata for this score, used when rendering Example ------- .. code-block:: python # create an endless score with a given time signature s = ScoreStruct(endless=True) s.addMeasure((4, 4), quarterTempo=72) # this is the same as: s = ScoreStruct.fromTimesig((4, 4), 72) # Create the beginning of Sacre s = ScoreStruct() s.addMeasure((4, 4), 50) s.addMeasure((3, 4)) s.addMeasure((4, 4)) s.addMeasure((2, 4)) s.addMeasure((3, 4), numMeasures=2) # The same can be achieved via a score string: s = ScoreStruct(r''' 4/4, 50 3/4 4/4 2/4 3/4 . ''') # Or everything in one line: s = ScoreStruct('4/4, 50; 3/4; 4/4; 2/4; 3/4; 3/4 ') **Format** A definitions are divided by new line or by ;. Each line has the form:: [measureindex,] timesig [, tempo] * Tempo refers by default to a quarter note (e.g. 60 indicates 60 bpm), can also be given as 4=60 or 8=60, in which case the resulting tempo is 30. * A tempo change without a timesignature change can be given as ``,<tempo>`` (e.g. ``,84``) * measure numbers start at 0 * comments start with `#` and last until the end of the line * A line with a single "." repeats the last defined measure * A score ending with the line ... is an endless score, which extends the last timesignature and tempo The measure number and/or the tempo can both be left out. The following definitions are all the same:: 1, 5/8, 63 5/8, 63 5/8 **Example**:: 0, 4/4, 60, "mark A" 3/4, 80 # Assumes measureIndex=1 10, 5/8, 120 30 . . # last measure (inclusive, the score will have 33 measures) """ def __init__(self, score: str | timesig_t = '', tempo: int | float | F = 0, endless: bool = True, title='', composer='', readonly=False, weightTempoThreshold: int | None = None, subdivTempoThreshold: int | None = None): # holds the time offset (in seconds) of each measure self._timeOffsets: list[F] = [] self._beatOffsets: list[F] = [] # the quarternote duration of each measure self._quarternoteDurations: list[F] = [] self._prevScoreStruct: ScoreStruct | None = None self._needsUpdate = True self._lastIndex = 0 self.readonly = False """Is this ScoreStruct read-only?""" self.title = title """Title metadata""" self.composer = composer """The composer metadata""" self.measuredefs: list[MeasureDef] = [] """The measure definitions""" self.endless = endless """Is this an endless scorestruct?""" self.readonly = readonly """Is this ScoreStruct read-only?""" self.beatWeightTempoThresh: int | None = weightTempoThreshold """Tempo at which a beat is considered strong, influences how syncopations and beams are broken""" self.subdivTempoThresh: int | None = subdivTempoThreshold """In an irregular measure, a beat resulting in a tempo slower than this is considered an independent beat""" self._hash: int | None = None """Cached hash""" if score: if isinstance(score, tuple): self.addMeasure(score, quarterTempo=tempo or 60) else: self._parseScore(score, initialTempo=tempo or 60) elif tempo: self.addMeasure((4, 4), quarterTempo=tempo) def __hash__(self) -> int: if self._hash is None: hashes = [hash(x) for x in (self.title, self.endless)] hashes.extend(hash(mdef) for mdef in self.measuredefs) self._hash = hash(tuple(hashes)) return self._hash def __eq__(self, other: ScoreStruct) -> int: return hash(self) == hash(other) def _parseScore(self, s: str, initialTempo=60, initialTimeSignature=(4, 4) ) -> None: """ Create a ScoreStruct from a string definition Args: s: the score as string. See below for format initialTempo: the initial tempo, for the case where the initial measure/s do not include a tempo initialTimeSignature: the initial time signature **Format** A definitions are divided by new line or by ;. Each line has the form:: measureIndex, timeSig, tempo * Tempo refers always to a quarter note * Any value can be left out: , 5/8, * measure numbers start at 0 * comments start with `#` and last until the end of the line * A line with a single "." repeats the last defined measure * A score ending with the line ... is an endless score The measure number and/or the tempo can both be left out. The following definitions are all the same:: 1, 5/8, 63 5/8, 63 5/8 **Example**:: 0, 4/4, 60, "mark A" ,3/4,80 # Assumes measureIndex=1 4/4 # Assumes measureIndex=2, inherits tempo 80 10, 5/8, 120 12,,96 # At measureIndex 12, change tempo to 96 30,, . . # last measure (inclusive, the score will have 33 measures) 10, 4/4 q=60 label='Mylabel' 3/4 q=42 20, q=60 label='foo' """ assert not self.measuredefs measureIndex = -1 lines = re.split(r'[\n;]', s) lines = emlib.textlib.linesStrip(lines) if lines[-1].strip() == '...': self.endless = True lines = lines[:-1] def lineStrip(line: str) -> str: if "#" in line: line = line.split("#")[0] return line.strip() for i, line0 in enumerate(lines): line = lineStrip(line0) if not line: continue if line == ".": if not self.measuredefs: raise ValueError("Cannot repeat last measure definition since there are " "no measures defined yet") self.addMeasure() measureIndex += 1 continue mdef = _parseScoreStructLine(line) if i == 0: if mdef.timesig is None: mdef.timesig = TimeSignature(initialTimeSignature) if mdef.tempo is None: mdef.tempo = initialTempo if mdef.measureIndex is None: mdef.measureIndex = measureIndex + 1 else: assert mdef.measureIndex > measureIndex if mdef.measureIndex - measureIndex > 1: self.addMeasure(numMeasures=mdef.measureIndex - measureIndex - 1) self.addMeasure( timesig=mdef.timesig or '', quarterTempo=mdef.tempo, annotation=mdef.label, rehearsalMark=RehearsalMark(mdef.rehearsalMark) if mdef.rehearsalMark else None, barline=mdef.barline ) measureIndex = mdef.measureIndex
[docs] def copy(self) -> ScoreStruct: """ Create a copy of this ScoreStruct """ s = ScoreStruct(endless=self.endless, title=self.title, composer=self.composer) s.measuredefs = [m.copy() for m in self.measuredefs] for m in s.measuredefs: m.parent = s s.modified() return s
[docs] def activate(self) -> Self: """ Set this scorestruct as active for the current workspace within maelzel.core .. seealso:: :func:`~maelzel.core.workspace.setScoreStruct` """ from maelzel.core import Workspace Workspace.active.scorestruct = self return self
[docs] def numMeasures(self) -> int: """ Returns the number of measures in this score structure If self is endless, it returns the number of defined measures Example ~~~~~~~ We create an endless structure (which is the default) where the last defined measure is at index 9. The number of measures is 10 >>> from maelzel.scorestruct import * >>> struct = ScoreStruct(r''' ... 4/4 ... . ... 3/4 ... 6, 4/4 ... 9, 3/4 ''') >>> struct.numMeasures() 10 """ return len(self.measuredefs)
def __len__(self): """ Returns the number of defined measures (even if the score is defined as endless) This is the same as :meth:`ScoreStruct.numMeasures` """ return self.numMeasures()
[docs] def getMeasureDef(self, idx: int, extend: bool | None = None) -> MeasureDef: """ Returns the MeasureDef at the given index. Args: idx: the measure index (measures start at 0) extend: if True and the index given is outside the defined measures, the score will be extended, repeating the last defined measure. For endless scores, the default is to extend the measure definitions. If the scorestruct is endless and the index is outside the defined range, the returned MeasureDef will be a copy of the last defined MeasureDef. The same result can be achieved via ``__getitem__`` Example ------- >>> from maelzel.scorestruct import ScoreStruct >>> s = ScoreStruct(r''' ... 4/4, 50 ... 3/4 ... 5/4, 72 ... 6/8 ... ''') >>> s.getMeasureDef(2) MeasureDef(timesig=(5, 4), quarterTempo=72, annotation='', timesigInherited=False, tempoInherited=True, barline='', subdivisionStructure=None) >>> s[2] MeasureDef(timesig=(5, 4), quarterTempo=72, annotation='', timesigInherited=False, tempoInherited=True, barline='', subdivisionStructure=None) """ self._update() if idx < len(self.measuredefs): measuredef = self.measuredefs[idx] assert measuredef.parent is self return measuredef if extend is None: extend = self.endless # outside defined measures if not extend: if not self.endless: raise IndexError(f"index {idx} out of range. The score has " f"{len(self.measuredefs)} measures defined") # "outside" of the defined score: return a copy of the last # measure so that any modification will not have any effect # Make the parent None so that it does not get notified if tempo or timesig # change out = self.measuredefs[-1].copy() out.readonly = True return out for n in range(len(self.measuredefs)-1, idx): self.addMeasure() mdef = self.measuredefs[-1] assert mdef.parent is self return mdef
@_overload def __getitem__(self, item: int) -> MeasureDef: ... @_overload def __getitem__(self, item: slice) -> list[MeasureDef]: ... def __getitem__(self, item): if isinstance(item, int): return self.getMeasureDef(item) assert isinstance(item, slice) return [self.getMeasureDef(idx) for idx in range(item.start, item.stop, item.step)]
[docs] def addMeasure(self, timesig: tuple[int, int] | str | TimeSignature = '', quarterTempo: num_t | None = None, index: int | None = None, annotation='', numMeasures=1, rehearsalMark: str | RehearsalMark | None = None, keySignature: tuple[int, str] | KeySignature | None = None, barline='', properties: dict[str, Any] | None = None, ) -> None: """ Add a measure definition to this score structure Args: timesig: the time signature of the new measure. If not given, the last time signature will be used. The timesig can be given as str in the form "num/den". For a compound time signature use "3/8+2/8". To specify the internal subdivision use a TimeSignature object or a string in the form "5/8(3-2)" quarterTempo: the tempo of a quarter note. If not given, the last tempo will be used annotation: each measure can have a text annotation index: if given, add measure at the given index numMeasures: if this is > 1, multiple measures of the same kind can be added rehearsalMark: if given, add a rehearsal mark to the new measure definition. A rehearsal mark can be a text or a RehearsalMark, which enables you to customize the rehearsal mark further keySignature: either a KeySignature object or a tuple (fifths, mode); for example for A-Major, ``(3, 'major')``. Mode can also be left as an ampty string barline: if needed, the right barline of the measure can be set to one of 'single', 'final', 'double', 'solid', 'dotted', 'dashed', 'tick', 'short', 'double-thin' or 'none' Example:: # Create a 4/4 score, 32 measures long >>> s = ScoreStruct() >>> s.addMeasure((4, 4), 52, numMeasures=32) """ if self.readonly: raise RuntimeError("This ScoreStruct is read-only") if not timesig: timesigInherited = True timesig = self.measuredefs[-1].timesig if self.measuredefs else (4, 4) else: timesigInherited = False if not quarterTempo: tempoInherited = True quarterTempo = self.measuredefs[-1].quarterTempo if self.measuredefs else F(60) else: tempoInherited = False if isinstance(rehearsalMark, str): rehearsalMark = RehearsalMark(rehearsalMark) if isinstance(keySignature, tuple): fifths, mode = keySignature assert isinstance(fifths, int) and isinstance(mode, str) keySignature = KeySignature(fifths=fifths, mode=mode) if not isinstance(timesig, TimeSignature): timesig = TimeSignature.parse(timesig) assert keySignature is None or isinstance(keySignature, KeySignature) measure = MeasureDef( timesig=timesig, quarterTempo=quarterTempo, annotation=annotation, timesigInherited=timesigInherited, tempoInherited=tempoInherited, rehearsalMark=rehearsalMark, properties=properties, keySignature=keySignature, barline=barline, parent=self, readonly=self.readonly) if index: if index < len(self.measuredefs): if numMeasures > 1: raise ValueError(f"Setting an already existing measure ({index}), multiple measures are not supported") self.measuredefs[index] = measure else: lastmeasure = self.measuredefs[-1].copy() lastmeasure.timesigInherited = True lastmeasure.tempoInherited = True for i in range(index - len(self.measuredefs)): self.measuredefs.append(lastmeasure.copy()) self.measuredefs.append(measure) else: self.measuredefs.append(measure) if numMeasures > 1: for _ in range(numMeasures - 1): self.measuredefs.append(MeasureDef( timesig=timesig, quarterTempo=quarterTempo, timesigInherited=True, tempoInherited=True, parent=self, readonly=self.readonly)) self.modified()
[docs] def addRehearsalMark(self, idx: int, mark: RehearsalMark | str, box: str = 'square' ) -> None: """ Add a rehearsal mark to this scorestruct The measure definition for the given index must already exist or the score must be set to autoextend Args: idx: the measure index mark: the rehearsal mark, as text or as a RehearsalMark box: one of 'square', 'circle' or '' to avoid drawing a box around the rehearsal mark """ if self.readonly: raise RuntimeError("This ScoreStruct is read-only") if idx >= len(self.measuredefs) and not self.endless: raise IndexError(f"Measure index {idx} out of range. " f"This score has {len(self.measuredefs)} measures") mdef = self.getMeasureDef(idx, extend=True) if isinstance(mark, str): mark = RehearsalMark(mark, box=box) mdef.rehearsalMark = mark
[docs] def ensureDurationInMeasures(self, numMeasures: int) -> None: """ Extends this score to have at least the given number of measures If the scorestruct already has reached the given length this operation does nothing Args: numMeasures: the minimum number of measures this score should have """ measureDiff = numMeasures - self.numMeasures() if measureDiff > 0: self.addMeasure(numMeasures=measureDiff)
[docs] def ensureDurationInSeconds(self, duration: F) -> None: """ Ensure that this scorestruct is long enough to include the given time This is of relevance in certain edge cases including endless scorestructs: * When creating a clicktrack from an endless score. * When exporting a scorestruct to midi Args: duration: the duration in seconds to ensure """ mindex, mbeat = self.timeToLocation(duration) if mindex is None: # Outside of this score's time range. assert not self.endless raise ValueError(f"duration {duration} outside score") self.ensureDurationInMeasures(mindex + 1)
[docs] def durationQuarters(self) -> F: """ The duration of this score, in quarternotes Raises ValueError if this score is endless """ if self.endless: raise ValueError("An endless score does not have a duration in beats") return asF(sum(m.durationQuarters for m in self.measuredefs))
[docs] def durationSecs(self) -> F: """ The duration of this score, in seconds Raises ValueError if this score is endless """ if self.endless: raise ValueError("An endless score does not have a duration in seconds") return asF(sum(m.durationSecs for m in self.measuredefs))
def _update(self) -> None: if not self._needsUpdate: return # if mdef := next((mdef for mdef in self.measuredefs if mdef.parent is not self), None): # raise ValueError(f"Wrong parent: {mdef}, parentid={id(mdef.parent)}, self={id(self)}") self._fixInheritedAttributes() accumTime = F(0) accumBeats = F(0) starts = [] quarterDurs = [] beatOffsets = [] for mdef in self.measuredefs: starts.append(accumTime) beatOffsets.append(accumBeats) durBeats = mdef.durationQuarters quarterDurs.append(durBeats) accumTime += F(60) / mdef.quarterTempo * durBeats accumBeats += durBeats self._timeOffsets = starts self._beatOffsets = beatOffsets self._quarternoteDurations = quarterDurs self._needsUpdate = False
[docs] def locationToTime(self, measure: int, beat: num_t = F(0)) -> F: """ Return the elapsed time at the given score location Args: measure: the measure number (starting with 0) beat: the beat within the measure Returns: a time in seconds (as a Fraction to avoid rounding problems) """ if self._needsUpdate: self._update() numdefs = len(self.measuredefs) if measure > numdefs - 1: if measure == numdefs and beat == 0: mdef = self.measuredefs[-1] return self._timeOffsets[-1] + mdef.durationSecs if not self.endless: raise ValueError("Measure outside score") last = numdefs - 1 lastTime = self._timeOffsets[last] mdef = self.measuredefs[last] mdur = mdef.durationSecs fractionalDur = beat * 60 / mdef.quarterTempo return lastTime + (measure - last) * mdur + fractionalDur else: now = self._timeOffsets[measure] mdef = self.measuredefs[measure] measureBeats = self._quarternoteDurations[measure] assert beat <= measureBeats, "Beat outside measure" qtempo = mdef.quarterTempo return now + F(60 * qtempo.denominator, qtempo.numerator) * beat
[docs] def tempoAtTime(self, time: num_t) -> F: """ Returns the tempo active at the given time (in seconds) Args: time: point in the timeline (in seconds) Returns: the quarternote-tempo at the given time """ measureindex, measurebeat = self.timeToLocation(time) if measureindex is None: raise ValueError(f"time {time} outside of score") measuredef = self.getMeasureDef(measureindex) return measuredef.quarterTempo
[docs] def timeToLocation(self, time: num_t) -> tuple[int | None, F]: """ Find the location in score corresponding to the given time in seconds Args: time: the time in seconds Returns: a tuple (measureindex, measurebeat) where measureindex can be None if the score is not endless and time is outside the score .. seealso:: :meth:`beatToLocation` """ if self._needsUpdate: self._update() if not self.measuredefs: raise IndexError("This ScoreStruct is empty") time = asF(time) idx = bisect(self._timeOffsets, time) if idx < len(self.measuredefs): m = self.measuredefs[idx-1] assert self._timeOffsets[idx - 1] <= time < self._timeOffsets[idx] dt = time-self._timeOffsets[idx-1] beat = dt*m.quarterTempo/F(60) return idx-1, beat # is it within the last measure? m = self.measuredefs[idx-1] dt = time - self._timeOffsets[idx-1] if dt < m.durationSecs: beat = dt*m.quarterTempo/F(60) return idx-1, beat # outside score if not self.endless: return None, F0 lastMeas = self.measuredefs[-1] measDur = lastMeas.durationSecs numMeasures = dt / measDur beat = (numMeasures - int(numMeasures)) * lastMeas.durationQuarters return len(self.measuredefs)-1 + int(numMeasures), beat
[docs] def beatToLocation(self, beat: num_t) -> tuple[int, F]: """ Return the location in score corresponding to the given beat The beat is the time-offset in quarter-notes. Given a beat (in quarter-notes), return the score location (measure, beat offset within the measure). Tempo does not play any role within this calculation. Returns: a tuple (measure index, beat). Raises ValueError if beat is not defined within this score .. note:: In the special case where a ScoreStruct is not endless and the beat is exactly at the end of the last measure, we return ``(numMeasures, 0)`` .. seealso:: :meth:`locationToBeat`, which performs the opposite operation Example ~~~~~~~ Given the following score: 4/4, 3/4, 4/4 ======== ======================= input output ======== ======================= 4 (1, 0) 5.5 (1, 1.5) 8 (2, 1.0) ======== ======================= """ if self._needsUpdate: self._update() numdefs = len(self.measuredefs) assert numdefs >= 1, "This scorestruct is empty" if not isinstance(beat, F): beat = asF(beat) lastBeatOffset = self._beatOffsets[-1] if beat > lastBeatOffset: # past the end rest = beat - lastBeatOffset if not self.endless: if rest > 0: raise ValueError(f"The given beat ({beat}) is outside the score") return (numdefs, F0) beatsPerMeasure = self.measuredefs[-1].durationQuarters numExtraMeasures = int(rest / beatsPerMeasure) idx = numdefs - 1 + numExtraMeasures restBeats = rest - numExtraMeasures*beatsPerMeasure # restBeats = rest % beatsPerMeasure return idx, restBeats else: lastIndex = self._lastIndex lastOffset = self._beatOffsets[lastIndex] if lastOffset <= beat < lastOffset + self._quarternoteDurations[lastIndex]: idx = lastIndex else: ridx = bisect(self._beatOffsets, beat) idx = ridx - 1 self._lastIndex = idx rest = beat - self._beatOffsets[idx] return idx, rest
[docs] def b2t(self, beat: num_t) -> F: """Beat to time""" meas, beat = self.beatToLocation(beat) return self.locationToTime(meas, beat)
[docs] def t2b(self, t: num_t) -> F: """Time to beat""" meas, beat = self.timeToLocation(t) if meas is None: raise ValueError(f"time {t} outside score") return self.locationToBeat(meas, beat)
[docs] def beatToTime(self, beat: num_t) -> F: """ Convert beat-time to real-time Args: beat: the quarter-note beat Returns: the corresponding time Example ~~~~~~~ >>> from maelzel.scorestruct import ScoreStruct >>> sco = ScoreStruct.fromTimesig('4/4', quarterTempo=120) >>> sco.beatToTime(2) 1.0 >>> sco.timeToBeat(2) 4.0 .. seealso:: :meth:`~ScoreStruct.timeToBeat` """ meas, offset = self.beatToLocation(beat) return self.locationToTime(meas, offset)
[docs] def remapTo(self, deststruct: ScoreStruct, location: num_t | tuple[int, num_t]) -> F: """ Remap a beat from this struct to another struct Args: location: the beat offset in quarternotes or a location (measureindex, offset) deststruct: the destination scores structure Returns: the beat within deststruct which keeps the same absolute time """ abstime = self.time(location) return deststruct.timeToBeat(abstime)
[docs] def remapSpan(self, sourcestruct: ScoreStruct, offset: num_t, duration: num_t ) -> tuple[F, F]: """ Remap a time span from a source score structure to this score structure Args: sourcestruct: the source score strcuture offset: the offset duration: the duration Returns: a tuple(offset, dur) where these represent the start and duration within this scorestruct which coincide in absolute time with the offset and duration given """ starttime = sourcestruct.beatToTime(offset) endtime = sourcestruct.beatToTime(offset + duration) startbeat = self.timeToBeat(starttime) endbeat = self.timeToBeat(endtime) return startbeat, endbeat - startbeat
[docs] def remapFrom(self, sourcestruct: ScoreStruct, location: num_t | tuple[int, num_t]) -> F: """ Remap a beat from sourcestruct to this this struct Args: location: the beat offset in quarternotes or a location (measureindex, offset) sourcestruct: the source score structure Returns: the beat within this struct which keeps the same absolute time as the given beat within sourcestruct """ abstime = sourcestruct.time(location) return self.timeToBeat(abstime)
[docs] def timeToBeat(self, t: num_t) -> F: """ Convert a time to a quarternote offset according to this ScoreStruct Args: t: the time (in absolute seconds) Returns: A quarternote offset will raise ValueError if the given time is outside this score structure Example ~~~~~~~ >>> from maelzel.scorestruct import ScoreStruct >>> sco = ScoreStruct.fromTimesig('4/4', quarterTempo=120) >>> sco.beatToTime(2) 1.0 >>> sco.timeToBeat(2) 4.0 .. seealso:: :meth:`~ScoreStruct.beatToTime` """ measureindex, measurebeat = self.timeToLocation(t) if measureindex is None: raise ValueError(f"time {t} outside score") beat = self.locationToBeat(measureindex, measurebeat) return beat
[docs] def iterMeasureDefs(self) -> Iterator[MeasureDef]: """ Iterate over all measure definitions in this ScoreStruct. If it is marked as `endless`, then the last defined measure will be returned indefinitely. """ for mdef in self.measuredefs: yield mdef if not self.endless: raise StopIteration lastmdef = self.measuredefs[-1] while True: yield lastmdef
def __iter__(self) -> Iterator[MeasureDef]: return self.iterMeasureDefs()
[docs] def beat(self, a: num_t | tuple[int, num_t], b: num_t | None = None ) -> F: """ Convert a time in secs or a location (measure, beat) to a quarter-note beat Args: a: the time/location to convert. Either a time b: when passign a location, the beat within the measure (`a` contains the measure index) Returns: the corresponding quarter note beat according to this ScoreStruct Example ~~~~~~~ >>> sco = ScoreStruct.fromTimesig('3/4', 120) # Convert time to beat >>> sco.beat(0.5) 1.0 # Convert score location (measure 1, beat 2) to beats >>> sco.beat((1, 2)) 5.0 # Also supported, similar to the previous operation: >>> sco.beat(1, 2) 5.0 .. seealso:: :meth:`~ScoreSctruct.time` """ if isinstance(a, tuple): assert b is None measureidx, beat = a return self.locationToBeat(measureidx, beat) elif b is not None: assert isinstance(a, int) return self.locationToBeat(a, b) else: return self.timeToBeat(a)
[docs] def time(self, a: num_t | tuple[int, num_t], b: num_t | None = None ) -> F: """ Convert a quarter-note beat or a location (measure, beat) to an absolute time in secs Args: a: the beat/location to convert. Either a beat, a tuple (measureindex, beat) or the measureindex itself, in which case `b` is also needed b: if given, then `a` is the measureindex and `b` is the beat Returns: the corresponding time according to this ScoreStruct Example ~~~~~~~ >>> sco = ScoreStruct.fromTimesig('3/4', 120) # Convert time to beat >>> sco.time(1) 0.5 # Convert score location (measure 1, beat 2) to beats >>> sco.time((1, 2)) 2.5 .. seealso:: :meth:`~ScoreSctruct.beat` """ if b is None: # a is a beat or a location if isinstance(a, tuple): measure, beat = a return self.locationToTime(measure, beat) else: return self.beatToTime(a) else: assert isinstance(a, int) return self.locationToTime(a, b)
[docs] def ltob(self, measure: int, beat: num_t) -> F: """ A shortcut to locationToBeat Args: measure: the measure index (measures start at 0 beat: the beat within the given measure Returns: the corresponding beat in quarter notes """ return self.locationToBeat(measure, beat)
[docs] def asBeat(self, location: num_t | tuple[int, float | F]) -> F: """ Given a beat or a location (measureidx, relativeoffset), returns an absolute beat Args: location: the location Returns: the absolute beat in quarter notes """ if isinstance(location, tuple): measure, beat = location return self.locationToBeat(measure, beat) else: return location if isinstance(location, F) else F(location) # type: ignore
[docs] def locationToBeat(self, measure: int, beat: num_t = F(0)) -> F: """ Returns the number of quarter notes up to the given location This value is independent of any tempo given. Args: measure: the measure number (measures start at 0) beat: the beat within the given measure (beat 0 = start of the measure), in quarter notes. Returns: the location translated to quarter notes. Example ------- >>> s = ScoreStruct(r''' ... 3/4, 120 ... 3/8 ... 4/4 ... ''') >>> s.locationToBeat(1, 0.5) 3.5 >>> s.locationToTime(1, 0.5) 1.75 """ if self._needsUpdate: self._update() if not isinstance(measure, int): raise TypeError(f"Expected a measure index, got {measure=}") if not isinstance(beat, (int, float, F)): raise TypeError(f"Expected a number as beat, got {beat=}") beat = asF(beat) if measure < self.numMeasures(): # Use the index measureOffset = self._beatOffsets[measure] quartersInMeasure = self._quarternoteDurations[measure] if beat > quartersInMeasure: raise ValueError(f"Measure {measure} has {quartersInMeasure} quarters, but given " f"offset {beat} is too large") return measureOffset + beat elif not self.endless: raise ValueError(f"This scorestruct has {self.numMeasures()} and is not" f"marked as endless. Measure {measure} is out of scope") # It is endless and out of the defined measures # TODO accum = F(0) for i, mdef in enumerate(self.iterMeasureDefs()): if i < measure: accum += mdef.durationQuarters else: if beat > mdef.durationQuarters: raise ValueError(f"beat {beat} outside measure {i}: {mdef}") accum += asF(beat) break return accum
[docs] def measureOffsets(self, startIndex=0, stopIndex=0) -> list[F]: """ Returns a list with the time offsets of each measure Args: startIndex: the measure index to start with. 0=last measure definition stopIndex: the measure index to end with (not included) Returns: a list of time offsets (start times), one for each measure in the interval selected """ if not stopIndex: stopIndex = self.numMeasures() return [self.locationToBeat(idx) for idx in range(startIndex, stopIndex)]
[docs] def measuresBetween(self, start: F, end: F) -> list[MeasureDef]: """ List of measures defined between the given times as beats Args: start: start beat in quarter-tones end: end beat in quarter-tones Returns: """ startloc = self.beatToLocation(start) idx0 = startloc[0] endloc = self.beatToLocation(end) idx1 = endloc[0] + 1 return [self.getMeasureDef(idx=i) for i in range(idx0, idx1)]
[docs] def timeDelta(self, start: beat_t, end: beat_t ) -> F: """ Returns the elapsed time between two beats or score locations. Args: start: the start location, as a beat or as a tuple (measureIndex, beatOffset) end: the end location, as a beat or as a tuple (measureIndex, beatOffset) Returns: the elapsed time, as a Fraction Example ------- >>> from maelzel.scorestruct import ScoreStruct >>> s = ScoreStruct('4/4,60; 3/4; 3/8') >>> s.timeDelta((0, 0.5), (2, 0.5)) 7 >>> s.timeDelta(3, (1, 2)) 3 .. seealso:: :meth:`~ScoreStruct.beatDelta` """ startTime = self.beatToTime(self.asBeat(start)) endTime = self.beatToTime(self.asBeat(end)) return endTime - startTime
[docs] def beatDelta(self, start: num_t | tuple[int, num_t], end: num_t | tuple[int, num_t]) -> F: """ Difference in beats between the two score locations or two times Args: start: the start moment as a location (a tuple (measureIndex, beatOffset) or as a time end: the end location, a tuple (measureIndex, beatOffset) Returns: the distance between the two locations, in beats Example ------- >>> from maelzel.scorestruct import ScoreStruct >>> s = ScoreStruct('4/4, 120; 3/4; 3/8; 5/8') # delta, in quarternotes, between time=2secs and location (2, 0) >>> s.beatDelta(2., (2, 0)) 5 .. seealso:: :meth:`~ScoreStruct.timeDelta` """ startBeat = self.beat(start) endBeat = self.beat(end) return endBeat - startBeat
[docs] def show(self, fmt='png', app='', scalefactor: float = 1.0, backend='', renderoptions: RenderOptions | None = None ) -> None: """ Render and show this ScoreStruct Args: fmt: the format to render to, one of 'png' or 'pdf' app: if given, the app used to open the produced document scalefactor: if given, a scale factor to enlarge or reduce the prduce image backend: the backend used (None to use a sensible default). If given, one of 'lilypond' or 'musicxml' renderoptions: if given, these options will be used for rendering this score structure as image. Example ~~~~~~~ >>> from maelzel.scorestruct import ScoreStruct >>> sco = ScoreStruct(r''' ... ... ... ''') >>> from maelzel.scoring.render import RenderOptions """ import tempfile from maelzel.core import environment import emlib.misc outfile = tempfile.mktemp(suffix='.' + fmt) self.write(outfile, backend=backend, renderoptions=renderoptions) if fmt == 'png': from maelzel.core import jupytertools if environment.insideJupyter and not app: jupytertools.jupyterShowImage(outfile, scalefactor=scalefactor, maxwidth=1200) else: emlib.misc.open_with_app(outfile, app=app or None) else: emlib.misc.open_with_app(outfile, app=app)
[docs] def dump(self, index=True, beatstruct=True) -> None: """ Dump this ScoreStruct to stdout """ self._update() from maelzel import _util if _util.pythonSessionType() == 'jupyter': from IPython.display import display, HTML display(HTML(self._repr_html_())) else: tempo = -1 for i, m in enumerate(self.measuredefs): parts = [] if index: parts.append(f"{i}: {m.timesig}") else: parts.append(str(m.timesig)) if m.quarterTempo != tempo: parts.append(f", {m.quarterTempo}") tempo = m.quarterTempo if m.annotation: parts.append(f", annotation={m.annotation}") if m.rehearsalMark: parts.append(f", rehearsal={m.rehearsalMark.text}") if m.barline: parts.append(f", barline={m.barline}") if m.keySignature: parts.append(f", keySignature={m.keySignature.fifths}") if beatstruct: beatstruct = m.beatStructure() s = "+".join(f"{beat.duration.numerator}/{beat.duration.denominator}" for beat in beatstruct) parts.append(f", beatstruct={s}") print("".join(parts))
[docs] def hasUniqueTempo(self) -> bool: """ Returns True if this ScoreStruct has no tempo changes """ self._update() t = self.measuredefs[0].quarterTempo return all(m.quarterTempo == t for m in self.measuredefs)
def __repr__(self) -> str: self._update() if self.hasUniqueTempo() and self.hasUniqueTimesig(): m0 = self.measuredefs[0] info = [str(m0.timesig), f"tempo={m0.quarterTempo}"] if m0.keySignature: info.append(f"keySignature={m0.keySignature}") infostr = ", ".join(info) return f'ScoreStruct({infostr})' else: tempo = -1 parts = [] maxdefs = 10 for m in self.measuredefs[:maxdefs]: if m.quarterTempo != tempo: tempo = m.quarterTempo parts.append(f"{m.timesig}@{tempo}") else: parts.append(f"{m.timesig}") s = ", ".join(parts) if len(self.measuredefs) > maxdefs: s += " …" return f"ScoreStruct([{s}])" def __enter__(self): from maelzel.core import getWorkspace w = getWorkspace() self._prevScoreStruct = w.scorestruct w.scorestruct = self return self def __exit__(self, exc_type, exc_val, exc_tb): assert self._prevScoreStruct is not None from maelzel.core import getWorkspace getWorkspace().scorestruct = self._prevScoreStruct def _repr_html_(self) -> str: self._update() import emlib.misc colnames = ['Meas. Index', 'Timesig', 'Tempo (quarter note)', 'Label', 'Rehearsal', 'Barline', 'Beats'] if any(m.keySignature is not None for m in self.measuredefs): colnames.append('Key') haskey = True else: haskey = False allparts = ['<p><strong>ScoreStruct</strong></p>'] tempo = -1 rows = [] for i, m in enumerate(self.measuredefs): # num, den = m.timesig if m.quarterTempo != tempo: tempo = m.quarterTempo tempostr = ("%.3f" % tempo).rstrip("0").rstrip(".") else: tempostr = "" rehearsal = m.rehearsalMark.text if m.rehearsalMark is not None else '' timesig = m.timesigRepr() if m.timesigInherited: timesig = f'({timesig})' beatstruct = m.beatStructure() parts = [] for beat in beatstruct: if beat.duration.denominator == 1: parts.append(str(beat.duration.numerator)) else: parts.append(f"{beat.duration.numerator}/{beat.duration.denominator}") s = "+".join(parts) row = [str(i), timesig, tempostr, m.annotation or "", rehearsal, m.barline, s] if haskey: row.append(str(m.keySignature.fifths) if m.keySignature else '-') rows.append(row) if self.endless: rows.append(("...", "", "", "", "", "")) rowstyle = 'font-size: small;' htmltable = emlib.misc.html_table(rows, colnames, rowstyles=[rowstyle]*len(colnames)) allparts.append(htmltable) return "".join(allparts) def _render(self, backend='', renderoptions: RenderOptions | None = None ) -> Renderer: self._update() from maelzel.scoring import quant from maelzel.scoring import render quantprofile = quant.QuantizationProfile() measures = [quant.QuantizedMeasure(timesig=m.timesig, quarterTempo=m.quarterTempo, quantprofile=quantprofile, beats=[]) for m in self.measuredefs] part = quant.QuantizedPart(struct=self, measures=measures, quantProfile=quantprofile) qscore = quant.QuantizedScore([part], title=self.title, composer=self.composer) if not renderoptions: renderoptions = render.RenderOptions() if backend: renderoptions.backend = backend return render.renderQuantizedScore(qscore, options=renderoptions)
[docs] def setTempo(self, tempo: float, reference=1, measureIndex: int = 0) -> None: """ Set the tempo of the given measure, until the next tempo change Args: tempo: the new tempo reference: the reference duration (1=quarternote, 2=halfnote, 0.5: 8th note, etc) measureIndex: the first measure to modify """ if self.readonly: raise RuntimeError("This ScoreStruct is read-only") if measureIndex > len(self) and not self.endless: raise IndexError(f"Index {measureIndex} out of rage; this ScoreStruct has only " f"{len(self)} measures defined") quarterTempo = asF(tempo) / asF(reference) mdef = self.getMeasureDef(measureIndex, extend=True) mdef.quarterTempo = quarterTempo mdef.tempoInherited = False for m in self.measuredefs[measureIndex+1:]: if m.tempoInherited: m.quarterTempo = quarterTempo else: break
[docs] def setTimeSignature(self, measureIndex, timesig: tuple[int, int] | str | TimeSignature ) -> None: if self.readonly: raise RuntimeError("This ScoreStruct is read-only") if measureIndex > len(self) and not self.endless: raise IndexError(f"Index {measureIndex} out of rage; this ScoreStruct has only " f"{len(self)} measures defined") timesig = _asTimeSignature(timesig) mdef = self.getMeasureDef(measureIndex, extend=True) mdef.timesig = timesig mdef.timesigInherited = False for m in self.measuredefs[measureIndex + 1:]: if m.timesigInherited: m.timesig = timesig else: break
[docs] def modified(self) -> None: """ mark this ScoreStruct as modified Example ~~~~~~~ >>> from maelzel.scorestruct import ScoreStruct >>> s = ScoreStruct(r''' ... 4/4, 60 ... 3/4 ... 4/4 ... . ... . ... 5/8 ... ''') >>> measure = s.getMeasureDef(3) >>> measure.timesig = 6/8 >>> s.modified() """ self._needsUpdate = True self._hash = None
def _fixInheritedAttributes(self): m0 = self.measuredefs[0] timesig = m0.timesig tempo = m0.quarterTempo for m in self.measuredefs[1:]: if m.tempoInherited: m._quarterTempo = tempo else: tempo = m._quarterTempo if m.timesigInherited: m._timesig = timesig else: timesig = m._timesig
[docs] def hasUniqueTimesig(self) -> bool: """ Returns True if this ScoreStruct does not have any time-signature change """ self._update() lastTimesig = self.measuredefs[0].timesig for m in self.measuredefs: if m.timesig != lastTimesig: return False return True
[docs] def write(self, path: str | Path, backend='', renderoptions: RenderOptions | None = None ) -> None: """ Export this score structure Write this as musicxml (.xml), lilypond (.ly), MIDI (.mid) or render as pdf or png. The format is determined by the extension of the file. It is also possible to write the score as text (in its own format) in order to load it later (.txt) .. note:: when saving as MIDI, notes are used to fill each beat because an empty MIDI score is not supported by the MIDI standard Args: path: the path of the written file backend: for pdf or png only - the backend to use for rendering, one of 'lilypond' or 'musicxml' renderoptions: if given, they will be used to customize the rendering process. """ self._update() path = Path(path) if path.suffix == ".xml": raise ValueError("musicxml output is not supported yet") elif path.suffix in (".pdf", '.png', '.ly'): r = self._render(backend=backend, renderoptions=renderoptions) r.write(str(path)) elif path.suffix == '.mid' or path.suffix == '.midi': sco = _filledScoreFromStruct(self) sco.write(str(path)) elif path.suffix == '.txt': text = self.asText() with open(path, 'w') as f: f.write(text) else: raise ValueError(f"Extension {path.suffix} not supported, " f"should be one of .xml, .pdf, .png or .ly")
[docs] def exportMidiClickTrack(self, midifile: str) -> None: """ Generate a MIDI click track from this ScoreStruct Args: midifile: the path of the MIDI file to generate .. seealso:: :func:`maelzel.core.clicktrack.makeClickTrack` """ from maelzel.core import clicktrack click = clicktrack.makeClickTrack(self) ext = Path(midifile).suffix.lower() if ext != '.mid' and ext != '.midi': raise ValueError(f"Expected a .mid or .midi extension, got {ext} ({midifile})") click.write(midifile)
[docs] def setEnd(self, numMeasures: int) -> None: """ Set an end measure to this ScoreStruct, in place If the scorestruct has less defined measures as requested, then it is extended by duplicating the last defined measure as needed. Otherwise, the scorestruct is cropped. The scorestruct ceases to be endless if that was the case previously Args: numMeasures: the requested number of measures after the operation """ self.endless = False if numMeasures < len(self.measuredefs): self.measuredefs = self.measuredefs[:numMeasures] else: last = self.measuredefs[-1] self.addMeasure(timesig=last.timesig, quarterTempo=last.quarterTempo, keySignature=last.keySignature, numMeasures=numMeasures - len(self.measuredefs))
[docs] def setBarline(self, measureIndex: int, linetype: str) -> None: """ Set the right barline type Args: measureIndex: the measure index to modify linetype: one of 'single', 'double', 'final', 'solid', 'dashed' """ if self.readonly: raise RuntimeError("This ScoreStruct is read-only") assert linetype in _barstyles, f"Unknown style '{linetype}', possible styles: {_barstyles}" self.getMeasureDef(measureIndex, extend=True).barline = linetype
[docs] def asText(self) -> str: """ This ScoreStruct as parsable text format Returns: this score as text """ lines = [] for i, measuredef in enumerate(self.measuredefs): line = measuredef.asScoreLine() lines.append(f'{i}, {line}') if self.endless: lines.append('...') return '\n'.join(lines)
[docs] def makeClickTrack(self, minMeasures: int = 0, clickdur: F | None = None, strongBeatPitch='5C', weakBeatPitch='5G', playTransposition=24, ) -> maelzel.core.Score: """ Create a click track from this ScoreStruct The returned score can be displayed as notation via :meth:`maelzel.core.Score.show` or exported as pdf or midi. This is a shortcut to :func:`maelzel.core.tools.makeClickTrack`. Use that for more customization options .. note:: The duration of the playback can be set individually from the duration of the displayed pitch Args: clickdur: the length of each tick. Use None to use the duration of the beat. strongBeatPitch: the pitch used as a strong beat (at the beginning of each measure) weakBeatPitch: the pitch used as a weak beat playTransposition: the transposition interval between notated pitch and playback pitch Returns: a maelzel.core.Score Example ------- >>> from maelzel.core import * >>> scorestruct = ScoreStruct(r"4/4,72; .; 5/8; 3/8; 2/4,96; .; 5/4; 3/4") >>> clicktrack = scorestruct.makeClickTrack() >>> clicktrack.write('click.pdf') >>> clicktrack.play() .. image:: ../assets/clicktrack2.png """ from maelzel.core.clicktrack import makeClickTrack if minMeasures < self.numMeasures(): struct = self else: struct = self.copy() struct.ensureDurationInMeasures(minMeasures) score = makeClickTrack(struct, clickdur=clickdur, strongBeatPitch=strongBeatPitch, weakBeatPitch=weakBeatPitch) score.setPlay(itransp=playTransposition) return score
def _filledScoreFromStruct(struct: ScoreStruct, pitch='4C') -> maelzel.core.Score: """ Creates a :class:`maelzel.core.score.Score` representing the given ScoreStruct Args: struct: the scorestruct to construct pitch: the pitch to use to fill the measures Returns: the resulting maelzel.core Score """ now = 0 events = [] from maelzel.core import Note, Voice, Score import pitchtools if isinstance(pitch, (int, float)): midinote = float(pitch) else: midinote = pitchtools.n2m(pitch) for i, measuredef in enumerate(struct.measuredefs): dur = measuredef.durationQuarters if i == len(struct.measuredefs) - 1: events.append(Note(pitch=midinote, offset=now, dur=dur)) now += dur voice = Voice(events) return Score([voice], scorestruct=struct)