Source code for abjadext.nauert.qschemas

import abc
import bisect
import copy

import abjad

from .qschemaitems import BeatwiseQSchemaItem, MeasurewiseQSchemaItem, QSchemaItem
from .qtargetitems import QTargetBeat, QTargetMeasure
from .qtargets import BeatwiseQTarget, MeasurewiseQTarget, QTarget
from .searchtrees import SearchTree, UnweightedSearchTree


[docs]class QSchema(abc.ABC): """ Abstract Q-schema. ``QSchema`` allows for the specification of quantization settings diachronically, at any time-step of the quantization process. In practice, this provides a means for the composer to change the tempo, search-tree, time-signature etc., effectively creating a template into which quantized rhythms can be "poured", without yet knowing what those rhythms might be, or even how much time the ultimate result will take. Like Abjad indicators the settings made at any given time-step via a ``QSchema`` instance are understood to persist until changed. All concrete ``QSchema`` subclasses strongly implement default values for all of their parameters. `QSchema` is abstract. """ ### CLASS VARIABLES ### __slots__ = ("_items", "_lookups") _keyword_argument_names: tuple[str, ...] = () _search_tree = UnweightedSearchTree() _tempo = abjad.MetronomeMark() ### INITIALIZER ### @abc.abstractmethod def __init__(self, *arguments, **keywords): if 1 == len(arguments) and isinstance(arguments[0], type(self)): items = copy.deepcopy(arguments[0].items) elif 1 == len(arguments) and isinstance(arguments[0], dict): items = list(arguments[0].items()) if abjad.math.all_are_pairs_of_types(items, int, dict): items = [(x, self.item_class(**y)) for x, y in items] assert abjad.math.all_are_pairs_of_types(items, int, self.item_class) items = dict(items) elif abjad.math.all_are_pairs_of_types(arguments, int, self.item_class): items = dict(arguments) elif abjad.math.all_are_pairs_of_types(arguments, int, dict): items = [(x, self.item_class(**y)) for x, y in arguments] items = dict(items) elif all(isinstance(x, self.item_class) for x in arguments): items = [(i, x) for i, x in enumerate(arguments)] items = dict(items) elif all(isinstance(x, dict) for x in arguments): items = [(i, self.item_class(**x)) for i, x in enumerate(arguments)] items = dict(items) else: raise ValueError if items: assert 0 <= min(items) self._items = dict(items) self._lookups = self._create_lookups() ### SPECIAL METHODS ###
[docs] def __call__(self, duration: abjad.typings.Duration) -> QTarget: """ Calls QSchema on ``duration``. """ target_items = [] idx, current_offset = 0, 0 duration = abjad.Duration(duration) while current_offset < duration: lookup = self[idx] lookup["offset_in_ms"] = current_offset target_item = self.target_item_class(**lookup) target_items.append(target_item) current_offset += target_item.duration_in_ms idx += 1 # assert all(isinstance(item, self.target_item_class) for item in target_items) return self.target_class(target_items)
[docs] def __getitem__(self, argument: int) -> dict: """ Gets item or slice identified by `argument`. """ assert isinstance(argument, int) and 0 <= argument result = {} for field in self._lookups: lookup = self._lookups[field].get(argument) if lookup is not None: result[field] = lookup else: keys = sorted(self._lookups[field].keys()) idx = bisect.bisect(keys, argument) if len(keys) == idx: key = keys[-1] elif argument < keys[idx]: key = keys[idx - 1] result[field] = self._lookups[field][key] return result
### PRIVATE METHODS ### def _create_lookups(self) -> dict[str, dict]: names = self._keyword_argument_names lookups = {} for name in names: lookups[name] = {0: getattr(self, name)} for position, item in self.items.items(): value = getattr(item, name) if value is not None: lookups[name][position] = value lookups[name] = dict(lookups[name]) return dict(lookups) ### PUBLIC PROPERTIES ### @abc.abstractproperty def item_class(self): """ The schema's item class. """ raise NotImplementedError @property def items(self) -> dict[int, QSchemaItem]: """ The item dictionary. """ return self._items @property def search_tree(self) -> SearchTree: """ The default search tree. """ return self._search_tree @abc.abstractproperty def target_class(self): """ The schema's target class. """ raise NotImplementedError @abc.abstractproperty def target_item_class(self): """ The schema's target class' item class. """ raise NotImplementedError @property def tempo(self) -> abjad.MetronomeMark: """ The default tempo. """ return self._tempo
[docs]class BeatwiseQSchema(QSchema): r""" Beatwise q-schema. Treats beats as timestep unit. >>> q_schema = nauert.BeatwiseQSchema() .. container:: example Without arguments, it uses smart defaults: >>> q_schema BeatwiseQSchema(beatspan=Duration(1, 4), search_tree=UnweightedSearchTree(definition={2: {2: {2: {2: None}, 3: None}, 3: None, 5: None, 7: None}, 3: {2: {2: None}, 3: None, 5: None}, 5: {2: None, 3: None}, 7: {2: None}, 11: None, 13: None}), tempo=MetronomeMark(reference_duration=Duration(1, 4), units_per_minute=60, textual_indication=None, custom_markup=None, decimal=False, hide=False)) .. container:: example Each time-step in a ``BeatwiseQSchema`` is composed of three settings: * ``beatspan`` * ``search_tree`` * ``tempo`` These settings can be applied as global defaults for the schema via keyword arguments, which persist until overridden: >>> beatspan = abjad.Duration(5, 16) >>> search_tree = nauert.UnweightedSearchTree({7: None}) >>> tempo = abjad.MetronomeMark(abjad.Duration(1, 4), 54) >>> q_schema = nauert.BeatwiseQSchema( ... beatspan=beatspan, ... search_tree=search_tree, ... tempo=tempo, ... ) .. container:: example The computed value at any non-negative time-step can be found by subscripting: >>> index = 0 >>> for key, value in sorted(q_schema[index].items()): ... print("{}:".format(key), value) ... beatspan: 5/16 search_tree: UnweightedSearchTree(definition={7: None}) tempo: MetronomeMark(reference_duration=Duration(1, 4), units_per_minute=54, textual_indication=None, custom_markup=None, decimal=False, hide=False) >>> index = 1000 >>> for key, value in sorted(q_schema[index].items()): ... print("{}:".format(key), value) ... beatspan: 5/16 search_tree: UnweightedSearchTree(definition={7: None}) tempo: MetronomeMark(reference_duration=Duration(1, 4), units_per_minute=54, textual_indication=None, custom_markup=None, decimal=False, hide=False) .. container:: example Per-time-step settings can be applied in a variety of ways. Instantiating the schema via ``*arguments`` with a series of either ``BeatwiseQSchemaItem`` instances, or dictionaries which could be used to instantiate ``BeatwiseQSchemaItem`` instances, will apply those settings sequentially, starting from time-step ``0``: >>> a = {"beatspan": abjad.Duration(5, 32)} >>> b = {"beatspan": abjad.Duration(3, 16)} >>> c = {"beatspan": abjad.Duration(1, 8)} >>> q_schema = nauert.BeatwiseQSchema(a, b, c) >>> q_schema[0]["beatspan"] Duration(5, 32) >>> q_schema[1]["beatspan"] Duration(3, 16) >>> q_schema[2]["beatspan"] Duration(1, 8) >>> q_schema[3]["beatspan"] Duration(1, 8) .. container:: example Similarly, instantiating the schema from a single dictionary, consisting of integer:specification pairs, or a sequence via ``*arguments`` of (integer, specification) pairs, allows for applying settings to non-sequential time-steps: >>> a = {"search_tree": nauert.UnweightedSearchTree({2: None})} >>> b = {"search_tree": nauert.UnweightedSearchTree({3: None})} >>> settings = { ... 2: a, ... 4: b, ... } >>> q_schema = nauert.BeatwiseQSchema(settings) >>> q_schema[0]["search_tree"] UnweightedSearchTree(definition={2: {2: {2: {2: None}, 3: None}, 3: None, 5: None, 7: None}, 3: {2: {2: None}, 3: None, 5: None}, 5: {2: None, 3: None}, 7: {2: None}, 11: None, 13: None}) >>> q_schema[1]["search_tree"] UnweightedSearchTree(definition={2: {2: {2: {2: None}, 3: None}, 3: None, 5: None, 7: None}, 3: {2: {2: None}, 3: None, 5: None}, 5: {2: None, 3: None}, 7: {2: None}, 11: None, 13: None}) >>> q_schema[2]["search_tree"] UnweightedSearchTree(definition={2: None}) >>> q_schema[3]["search_tree"] UnweightedSearchTree(definition={2: None}) >>> q_schema[4]["search_tree"] UnweightedSearchTree(definition={3: None}) >>> q_schema[1000]["search_tree"] UnweightedSearchTree(definition={3: None}) .. container:: example The following is equivalent to the above schema definition: >>> q_schema = nauert.BeatwiseQSchema( ... (2, {"search_tree": nauert.UnweightedSearchTree({2: None})}), ... (4, {"search_tree": nauert.UnweightedSearchTree({3: None})}), ... ) """ ### CLASS VARIABLES ### __slots__ = ("_beatspan", "_items", "_lookups", "_search_tree", "_tempo") _keyword_argument_names = ("beatspan", "search_tree", "tempo") ### INITIALIZER ### def __init__(self, *arguments, **keywords): self._beatspan = abjad.Duration(keywords.get("beatspan", (1, 4))) search_tree = keywords.get("search_tree", UnweightedSearchTree()) assert isinstance(search_tree, SearchTree) self._search_tree = search_tree tempo = keywords.get("tempo", (abjad.Duration(1, 4), 60)) if isinstance(tempo, tuple): tempo = abjad.MetronomeMark(*tempo) self._tempo = tempo QSchema.__init__(self, *arguments, **keywords)
[docs] def __repr__(self): """ Gets repr. """ return f"{type(self).__name__}(beatspan={self.beatspan!r}, search_tree={self.search_tree!r}, tempo={self.tempo!r})"
### PUBLIC PROPERTIES ### @property def beatspan(self) -> abjad.Duration: """ Default beatspan of beatwise q-schema. """ return self._beatspan @property def item_class(self) -> type[BeatwiseQSchemaItem]: """ The schema's item class. """ return BeatwiseQSchemaItem @property def target_class(self) -> type[BeatwiseQTarget]: """ Target class of beatwise q-schema. """ return BeatwiseQTarget @property def target_item_class(self) -> type[QTargetBeat]: """ Target item class of beatwise q-schema. """ return QTargetBeat
[docs]class MeasurewiseQSchema(QSchema): r""" Measurewise q-schema. Treats measures as its timestep unit. >>> q_schema = nauert.MeasurewiseQSchema() .. container:: example Without arguments, it uses smart defaults: >>> q_schema MeasurewiseQSchema(search_tree=UnweightedSearchTree(definition={2: {2: {2: {2: None}, 3: None}, 3: None, 5: None, 7: None}, 3: {2: {2: None}, 3: None, 5: None}, 5: {2: None, 3: None}, 7: {2: None}, 11: None, 13: None}), tempo=MetronomeMark(reference_duration=Duration(1, 4), units_per_minute=60, textual_indication=None, custom_markup=None, decimal=False, hide=False), time_signature=TimeSignature(pair=(4, 4), hide=False, partial=None), use_full_measure=False) .. container:: example Each time-step in a ``MeasurewiseQSchema`` is composed of four settings: * ``search_tree`` * ``tempo`` * ``time_signature`` * ``use_full_measure`` These settings can be applied as global defaults for the schema via keyword arguments, which persist until overridden: >>> search_tree = nauert.UnweightedSearchTree({7: None}) >>> time_signature = abjad.TimeSignature((3, 4)) >>> tempo = abjad.MetronomeMark(abjad.Duration(1, 4), 54) >>> use_full_measure = True >>> q_schema = nauert.MeasurewiseQSchema( ... search_tree=search_tree, ... tempo=tempo, ... time_signature=time_signature, ... use_full_measure=use_full_measure, ... ) All of these settings are self-descriptive, except for ``use_full_measure``, which controls whether the measure is subdivided by the ``quantize`` function into beats according to its time signature. If ``use_full_measure`` is ``False``, the time-step's measure will be divided into units according to its time-signature. For example, a 4/4 measure will be divided into 4 units, each having a beatspan of 1/4. On the other hand, if ``use_full_measure`` is set to ``True``, the time-step's measure will not be subdivided into independent quantization units. This usually results in full-measure tuplets. .. container:: example The computed value at any non-negative time-step can be found by subscripting: >>> index = 0 >>> for key, value in sorted(q_schema[index].items()): ... print("{}:".format(key), value) ... search_tree: UnweightedSearchTree(definition={7: None}) tempo: MetronomeMark(reference_duration=Duration(1, 4), units_per_minute=54, textual_indication=None, custom_markup=None, decimal=False, hide=False) time_signature: TimeSignature(pair=(3, 4), hide=False, partial=None) use_full_measure: True >>> index = 1000 >>> for key, value in sorted(q_schema[index].items()): ... print("{}:".format(key), value) ... search_tree: UnweightedSearchTree(definition={7: None}) tempo: MetronomeMark(reference_duration=Duration(1, 4), units_per_minute=54, textual_indication=None, custom_markup=None, decimal=False, hide=False) time_signature: TimeSignature(pair=(3, 4), hide=False, partial=None) use_full_measure: True .. container:: example Per-time-step settings can be applied in a variety of ways. Instantiating the schema via ``*arguments`` with a series of either ``MeasurewiseQSchemaItem`` instances, or dictionaries which could be used to instantiate ``MeasurewiseQSchemaItem`` instances, will apply those settings sequentially, starting from time-step ``0``: >>> a = {"search_tree": nauert.UnweightedSearchTree({2: None})} >>> b = {"search_tree": nauert.UnweightedSearchTree({3: None})} >>> c = {"search_tree": nauert.UnweightedSearchTree({5: None})} >>> q_schema = nauert.MeasurewiseQSchema(a, b, c) >>> q_schema[0]["search_tree"] UnweightedSearchTree(definition={2: None}) >>> q_schema[1]["search_tree"] UnweightedSearchTree(definition={3: None}) >>> q_schema[2]["search_tree"] UnweightedSearchTree(definition={5: None}) >>> q_schema[1000]["search_tree"] UnweightedSearchTree(definition={5: None}) .. container:: example Similarly, instantiating the schema from a single dictionary, consisting of integer:specification pairs, or a sequence via ``*arguments`` of (integer, specification) pairs, allows for applying settings to non-sequential time-steps: >>> a = {"time_signature": abjad.TimeSignature((7, 32))} >>> b = {"time_signature": abjad.TimeSignature((3, 4))} >>> c = {"time_signature": abjad.TimeSignature((5, 8))} >>> settings = { ... 2: a, ... 4: b, ... 6: c, ... } >>> q_schema = nauert.MeasurewiseQSchema(settings) >>> q_schema[0]["time_signature"] TimeSignature(pair=(4, 4), hide=False, partial=None) >>> q_schema[1]["time_signature"] TimeSignature(pair=(4, 4), hide=False, partial=None) >>> q_schema[2]["time_signature"] TimeSignature(pair=(7, 32), hide=False, partial=None) >>> q_schema[3]["time_signature"] TimeSignature(pair=(7, 32), hide=False, partial=None) >>> q_schema[4]["time_signature"] TimeSignature(pair=(3, 4), hide=False, partial=None) >>> q_schema[5]["time_signature"] TimeSignature(pair=(3, 4), hide=False, partial=None) >>> q_schema[6]["time_signature"] TimeSignature(pair=(5, 8), hide=False, partial=None) >>> q_schema[1000]["time_signature"] TimeSignature(pair=(5, 8), hide=False, partial=None) .. container:: example The following is equivalent to the above schema definition: >>> q_schema = nauert.MeasurewiseQSchema( ... (2, {"time_signature": abjad.TimeSignature((7, 32))}), ... (4, {"time_signature": abjad.TimeSignature((3, 4))}), ... (6, {"time_signature": abjad.TimeSignature((5, 8))}), ... ) """ ### CLASS VARIABLES ### __slots__ = ( "_items", "_lookups", "_search_tree", "_tempo", "_time_signature", "_use_full_measure", ) _keyword_argument_names = ( "search_tree", "tempo", "time_signature", "use_full_measure", ) ### INITIALIZER ### def __init__(self, *arguments, **keywords): search_tree = keywords.get("search_tree", UnweightedSearchTree()) assert isinstance(search_tree, SearchTree) self._search_tree = search_tree tempo = keywords.get("tempo", (abjad.Duration(1, 4), 60)) if isinstance(tempo, tuple): tempo = abjad.MetronomeMark(*tempo) self._tempo = tempo time_signature = keywords.get("time_signature", (4, 4)) if isinstance(time_signature, abjad.TimeSignature): self._time_signature = abjad.TimeSignature(time_signature.pair) elif isinstance(time_signature, tuple): self._time_signature = abjad.TimeSignature(time_signature) else: raise TypeError(time_signature) self._use_full_measure = bool(keywords.get("use_full_measure")) QSchema.__init__(self, *arguments, **keywords)
[docs] def __repr__(self): """ Gets repr. """ return f"{type(self).__name__}(search_tree={self.search_tree!r}, tempo={self.tempo!r}, time_signature={self.time_signature!r}, use_full_measure={self.use_full_measure})"
### PUBLIC PROPERTIES ### @property def item_class(self) -> type[MeasurewiseQSchemaItem]: """ Item class of measurewise q-schema. """ return MeasurewiseQSchemaItem @property def target_class(self) -> type[MeasurewiseQTarget]: """ Target class of measurewise q-schema. """ return MeasurewiseQTarget @property def target_item_class(self) -> type[QTargetMeasure]: """ Target item class of measurewise q-schema. """ return QTargetMeasure @property def time_signature(self) -> abjad.TimeSignature: """ Default time signature of measurewise q-schema. .. container:: example >>> q_schema = nauert.MeasurewiseQSchema( ... time_signature=abjad.TimeSignature((3, 4)) ... ) >>> q_schema.time_signature TimeSignature(pair=(3, 4), hide=False, partial=None) .. container:: example If there are multiple time signatures in the QSchema, this returns the default time signature of (4, 4). >>> a = {"time_signature": abjad.TimeSignature((7, 32))} >>> b = {"time_signature": abjad.TimeSignature((3, 4))} >>> c = {"time_signature": abjad.TimeSignature((5, 8))} >>> settings = { ... 2: a, ... 4: b, ... 6: c, ... } >>> q_schema = nauert.MeasurewiseQSchema(settings) >>> q_schema.time_signature TimeSignature(pair=(4, 4), hide=False, partial=None) """ return self._time_signature @property def use_full_measure(self) -> bool: """ The full-measure-as-beatspan default. """ return self._use_full_measure