import random
from typing import Optional, Union
import abjad
from .. import mutate
from .TenneySelector import TenneySelector
[docs]class PitchRandomiser:
r"""Takes an input |abjad.Container| (or child class) together with a list
of pitches and randomises the container's pitch content using the list of
pitches. The pitches can be of type :obj:`list`, :obj:`tuple`, :obj:`str`,
or |abjad.PitchSegment|.
Basic usage:
Calling the object will output a selection of the input container with
randomised pitches. Pitches are randomly selected from :attr:`pitches`.
>>> container = abjad.Container(r"\time 4/4 c'4 d'4 e'4 f'4")
>>> abjad.show(container)
.. docs::
{
c'4
d'4
e'4
f'4
}
.. figure:: ../_images/PitchRandomiser-PxSLuwtgn9.png
>>> pitches = r"fs' gs' a' b' cs''"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
a'4
b'4
cs''4
fs'4
}
.. figure:: ../_images/PitchRandomiser-134lqskbb6o.png
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
b'4
b'4
b'4
fs'4
}
.. figure:: ../_images/PitchRandomiser-z66g1fy8nm8.png
To get the result of the last operation, use the property
:attr:`current_window`.
>>> notes = randomiser.current_window
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
b'4
b'4
b'4
fs'4
}
.. figure:: ../_images/PitchRandomiser-x0e6yduogh.png
.. warning::
Unlike the other classes in Auxjad, the very first call of this class
will already process the initial container. To disable this behaviour
and output the initial container once before randomising its pitches,
initialise the class with the keyword argument
:attr:`process_on_first_call` set to ``False``.
>>> container = abjad.Container(r"c'4 d'4 e'4 f'4")
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... process_on_first_call=False,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
c'4
d'4
e'4
f'4
}
.. figure:: ../_images/PitchRandomiser-640x6vsjwtk.png
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
b'4
fs'4
gs'4
fs'4
}
.. figure:: ../_images/PitchRandomiser-vsjdj8gkanj.png
:func:`len()`:
Applying the :func:`len()` function to the randomiser will return the
number of pitches in :attr:`pitches`.
>>> container = abjad.Container(r"c'4 d'4 e'4 f'4")
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> len(randomiser)
4
>>> container = abjad.Container(r"c'4 d'4 e'4 f'4")
>>> pitches = [6, 7, 8, 9, 10, 11, 12]
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> len(randomiser)
7
Arguments and properties:
This class has many keyword arguments, all of which can be altered
after instantiation using properties with the same names as shown
below. :attr:`weights` takes a :obj:`list` of :obj:`int`'s or
:obj:`float`'s representing the weight of each pitch from
:attr:`pitches` (their lengths must also match).
:attr:`omit_time_signatures` will remove all time signatures from the
output (both are ``False`` by default). :attr:`process_on_first_call`
to ``True`` and the random pitch process will be applied on the very
first call. Setting :attr:`use_tenney_selector` to ``True`` will make
the randomiser use :class:`auxjad.TenneySelector` for the random
selection instead of :func:`random.choices()` (default is ``False``).
>>> container = abjad.Container(r"c'4 d'4 e'4 f'4")
>>> randomiser = auxjad.PitchRandomiser(
... container,
... pitches=r"a b cs' ds' e'",
... weights=[1.0, 2.0, 1.0, 1.5, 1.3],
... omit_time_signatures=True,
... process_on_first_call=True,
... use_tenney_selector=True,
... )
>>> randomiser.pitches
<a b cs' ds' e'>
>>> randomiser.weights
[1.0, 2.0, 1.0, 1.5, 1.3]
>>> randomiser.omit_time_signatures
True
>>> randomiser.process_on_first_call
True
>>> randomiser.use_tenney_selector
True
Use the properties below to change these values after initialisation.
>>> randomiser.pitches = abjad.PitchSegment(r"c' d' e' f'")
>>> randomiser.weights = [1, 2, 5, 8]
>>> randomiser.omit_time_signatures = False
>>> randomiser.process_on_first_call = False
>>> randomiser.use_tenney_selector = False
>>> randomiser.pitches
<c' d' e' f'>
>>> randomiser.weights
[1, 2, 5, 8]
>>> randomiser.omit_time_signatures
False
>>> randomiser.process_on_first_call
False
>>> randomiser.use_tenney_selector
False
Rests:
Only pitched logical ties are randomised, rests are left untouched.
>>> container = abjad.Container(r"c'8. d'4 r8 r8. e'16 f'8.")
>>> pitches = [6, 7, 8, 9, 10, 11]
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
bf'8.
af'4
r8
r8.
bf'16
a'8.
}
.. figure:: ../_images/PitchRandomiser-u4294ozm92.png
Chords:
Each note of a chord is randomised too.
>>> container = abjad.Container(
... r"<c' e' g'>8. d'4 r8 r8. e'16 <f' a'>8."
... )
>>> pitches = [6, 7, 8, 9, 10, 11]
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
<fs' g' bf'>8.
b'4
r8
r8.
bf'16
<fs' bf'>8.
}
.. figure:: ../_images/PitchRandomiser-318eldj7tzc.png
The number of notes in a chord stay the same unless there are fewer
pitches available in :attr:`pitches`.
>>> container = abjad.Container(
... r"<c' e' g' a'>2 <cs' ds' e' f' g' a' b'>2"
... )
>>> pitches = [6, 7, 8]
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
<fs' g' af'>2
<fs' g' af'>2
}
.. figure:: ../_images/PitchRandomiser-cciyee49qrj.png
:attr:`use_tenney_selector`:
Setting :attr:`use_tenney_selector` to ``True`` will make the
randomiser use :class:`auxjad.TenneySelector` for the random selection
instead of :func:`random.choices()` (default is ``False``).
:class:`auxjad.TenneySelector` will raise the chance of a pitch being
selected the longer it hasn't been selected, and will forbid immediate
repetitions of pitches. See its documentation for more information.
>>> container = abjad.Container(r"c'8 d'8 e'8 f'8 g'8 a'8 b'8 c'8")
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... use_tenney_selector=True,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
fs'8
a'8
fs'8
gs'8
a'8
b'8
gs'8
fs'8
}
.. figure:: ../_images/PitchRandomiser-tmxllu13pa8.png
:attr:`weights`:
Individual pitches can have different weights, defined by the
:attr:`weights` property. It takes a :obj:`list` of :obj:`float`'s or
:obj:`int`'s.
>>> container = abjad.Container(r"c'8 d'8 e'8 f'8 g'8 a'8 b'8 c'8")
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... weights=[5.0, 2.0, 1.5, 1.0],
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
b'8
fs'8
gs'8
gs'8
gs'8
fs'8
fs'8
a'8
}
.. figure:: ../_images/PitchRandomiser-zkvbzd1brgq.png
:attr:`weights` and :attr:`use_tenney_selector`:
Non-uniform :attr:`weights` can also be used when
:attr:`use_tenney_selector` is set to ``True``.
>>> container = abjad.Container(r"c'8 d'8 e'8 f'8 g'8 a'8 b'8 c'8")
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... weights=[5.0, 2.0, 1.5, 1.0],
... use_tenney_selector=True,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
fs'8
gs'8
a'8
fs'8
gs'8
a'8
gs'8
b'8
}
.. figure:: ../_images/PitchRandomiser-cq1nobkjozg.png
Resetting :attr:`weights`:
Setting :attr:`weights` to ``None`` will reset it back to a uniform
distribution.
>>> container = abjad.Container(r"c'8 d'8 e'8 f'8 g'8 a'8 b'8 c'8")
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... weights=[100.0, 1.0, 1.0, 1.0],
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
fs'8
fs'8
fs'8
fs'8
fs'8
fs'8
fs'8
fs'8
}
.. figure:: ../_images/PitchRandomiser-wtl5o15q5qp.png
>>> randomiser.weights = None
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
fs'8
fs'8
b'8
gs'8
gs'8
a'8
fs'8
b'8
}
.. figure:: ../_images/PitchRandomiser-4bqe6cbawg6.png
Changing :attr:`pitches`:
When using a custom :obj:`list` of :attr:`weights`, changing the
:attr:`pitches` to a series of new values with the same length will
preserve the :attr:`weights` values. If on the other hand
:attr:`pitches` changes in length, :attr:`weights` is reset to ``None``
(i.e. uniform distribution).
>>> container = abjad.Container(r"c'8 d'8 e'8 f'8 g'8 a'8 b'8 c'8")
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... weights=[100.0, 1.0, 1.0, 1.0],
... )
>>> randomiser.pitches = r"c'' d'' e'' f''"
>>> randomiser.pitches
<c'' d'' e'' f''>
>>> randomiser.weights
[100.0, 1.0, 1.0, 1.0]
>>> randomiser.pitches = r"c'' d'' e'' f'' g'' a'' b''"
>>> randomiser.pitches
<c'' d'' e'' f'' g'' a'' b''>
>>> randomiser.weights
None
.. error::
Note that :attr:`weights` must always have the same length as
:attr:`pitches`, otherwise a :exc:`ValueError` exception will be
raised.
>>> container = abjad.Container(r"c'4 d'4 e'4 f'4")
>>> pitches = r"fs' gs' a' b'"
>>> weights = [1, 1, 5, 2, 3, 4, 8]
>>> auxjad.PitchRandomiser(container, pitches, weights=weights)
ValueError: 'weights' must have the same length as 'pitches'
:meth:`output_n`:
To output several randomised containers at once, use the method
:meth:`output_n`, inputting the desired number of iterations.
>>> container = abjad.Container(r"c'4 ~ c'16 r8. d'4 e'8. r16")
>>> pitches = [6, 7, 8, 9, 10]
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> notes = randomiser.output_n(3)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
a'4
~
a'16
r8.
g'4
fs'8.
r16
g'4
~
g'16
r8.
g'4
fs'8.
r16
bf'4
~
bf'16
r8.
a'4
af'8.
r16
}
.. figure:: ../_images/PitchRandomiser-fvwaaz3vgi.png
Indicators:
This class preserves indicators.
>>> container = abjad.Container(
... r"c'4\p\< ~ c'8. d'16-.\f e'4--\pp f'8.( g'16)"
... )
>>> pitches = [6, 7, 8, 9, 10, 11, 12]
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
a'4
\p
\<
~
a'8.
c''16
\f
- \staccato
af'4
\pp
- \tenuto
a'8.
(
bf'16
)
}
.. figure:: ../_images/PitchRandomiser-2e647eng8yc.png
Example:
This class also preserves the time signature structure.
>>> container = abjad.Container(
... r"\time 3/4 c'4 d'2 \time 2/4 e'8 f'8 g'8 a'8"
... )
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 3/4
a'4
fs'2
\time 2/4
gs'8
gs'8
a'8
gs'8
}
.. figure:: ../_images/PitchRandomiser-c9t7r3thrqg.png
:attr:`omit_time_signatures`:
To omit time signatures altogether, set :attr:`omit_time_signatures` to
``True`` (default is ``False``).
>>> container = abjad.Container(
... r"\time 3/4 c'4 d'2 \time 2/4 e'8 f'8 g'8 a'8"
... )
>>> pitches = r"fs' gs' a' b'"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... omit_time_signatures=True,
... )
>>> notes = randomiser()
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
a'4
fs'2
gs'8
gs'8
a'8
gs'8
}
.. figure:: ../_images/PitchRandomiser-mwruvbmgu3o.png
Using as iterator:
The instances of this class can also be used as an iterator, which can
then be used in a for loop. Note that unlike the method
:meth:`output_n`, time signatures are added to each window returned by
the randomiser. Use the function
|auxjad.mutate.remove_repeated_time_signatures()| to clean the output
when using this class in this way. It is also important to note that a
``break`` statement is needed when using this class as an iterator. The
reason is that pitch randomisation is a process that can happen
indefinitely (unlike some of the other classes in this library).
>>> container = abjad.Container(r"\time 3/4 c'4 d'4 e'4")
>>> pitches = r"fs' gs' a' b' cs''"
>>> randomiser = auxjad.PitchRandomiser(container,
... pitches,
... )
>>> staff = abjad.Staff()
>>> for window in randomiser:
... staff.append(window)
... if abjad.get.duration(staff) == abjad.Duration((9, 4)):
... break
>>> auxjad.mutate.remove_repeated_time_signatures(staff)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 3/4
a'4
b'4
gs'4
fs'4
a'4
b'4
a'4
gs'4
cs''4
}
.. figure:: ../_images/PitchRandomiser-2c9zzip8tpc.png
.. tip::
The functions |auxjad.mutate.remove_repeated_dynamics()| and
|auxjad.mutate.reposition_clefs()| can be used to clean the output
and remove repeated dynamics and unnecessary clef changes.
"""
### CLASS VARIABLES ###
__slots__ = ('_contents',
'_pitches',
'_weights',
'_omit_time_signatures',
'_process_on_first_call',
'_use_tenney_selector',
'_tenney_selector',
'_current_window',
'_is_first_window',
)
### INITIALISER ###
[docs] def __init__(self,
contents: abjad.Container,
pitches: Union[list[Union[int, float, str, abjad.Pitch]],
tuple[Union[int, float, str, abjad.Pitch]],
str,
abjad.PitchSegment,
],
*,
weights: Optional[list] = None,
omit_time_signatures: bool = False,
process_on_first_call: bool = True,
use_tenney_selector: bool = False,
) -> None:
r'Initialises self.'
self.contents = contents
self._weights = []
self.pitches = pitches
self.weights = weights
self.omit_time_signatures = omit_time_signatures
self.process_on_first_call = process_on_first_call
self.use_tenney_selector = use_tenney_selector
self._is_first_window = True
### SPECIAL METHODS ###
[docs] def __repr__(self) -> str:
r'Returns interpreter representation of :attr:`pitches`.'
return repr(self._pitches)
[docs] def __len__(self) -> int:
r'Returns the number of available :attr:`pitches`.'
return len(self._pitches)
[docs] def __call__(self) -> abjad.Selection:
r'Calls the randomise process, returning an |abjad.Selection|'
self._randomise()
return self.current_window
[docs] def __next__(self) -> abjad.Selection:
r"""Calls the randomise process for one iteration, returning an
|abjad.Selection|.
"""
return self.__call__()
[docs] def __iter__(self) -> None:
r'Returns an iterator, allowing instances to be used as iterators.'
return self
### PUBLIC METHODS ###
[docs] def output_n(self,
n: int,
) -> abjad.Selection:
r"""Goes through ``n`` iterations of the pitch randomisation process
and outputs a single |abjad.Selection|.
"""
if not isinstance(n, int):
raise TypeError("argument must be 'int'")
if n < 1:
raise ValueError("argument must be greater than zero")
dummy_container = abjad.Container()
for _ in range(n):
dummy_container.append(self.__call__())
mutate.remove_repeated_time_signatures(dummy_container[:])
mutate.remove_repeated_dynamics(dummy_container[:])
output = dummy_container[:]
dummy_container[:] = []
return output
### PRIVATE METHODS ###
def _randomise(self) -> abjad.Selection:
r'Randomises pitches of :attr:`contents`.'
if self._is_first_window and not self._process_on_first_call:
self._is_first_window = False
else:
self._rewrite_pitches()
def _rewrite_pitches(self) -> None:
r'Rewrites the pitches of the current window.'
dummy_container = abjad.mutate.copy(self._contents)
logical_ties = abjad.select(dummy_container).logical_ties()
for logical_tie in logical_ties:
if isinstance(logical_tie[0], abjad.Note):
pitch = self._pick_random_pitch()
for leaf in logical_tie:
leaf.written_pitch = pitch
elif isinstance(logical_tie[0], abjad.Chord):
chord_n = len(logical_tie[0].written_pitches)
if chord_n > self.__len__():
pitches = self._pitches
else:
pitches = []
while len(pitches) < chord_n:
pitch = self._pick_random_pitch()
if pitch not in pitches:
pitches.append(pitch)
for leaf in logical_tie:
leaf.written_pitches = pitches
# output
self._is_first_window = False
self._current_window = dummy_container[:]
dummy_container[:] = []
def _pick_random_pitch(self) -> abjad.Pitch:
r"""Random pitch selector, using either :func:`random.choices()` or
:class:`auxjad.TenneySelector`.
"""
if not self._use_tenney_selector:
return random.choices(self._pitches,
weights=self._weights,
)[0]
else:
return self._tenney_selector()
@staticmethod
def _remove_all_time_signatures(container) -> None:
r'Removes all time signatures of an |abjad.Container|.'
for leaf in abjad.select(container).leaves():
if abjad.get.effective(leaf, abjad.TimeSignature):
abjad.detach(abjad.TimeSignature, leaf)
### PUBLIC PROPERTIES ###
@property
def contents(self) -> abjad.Container:
r'The |abjad.Container| to be shuffled.'
return abjad.mutate.copy(self._contents)
@contents.setter
def contents(self,
contents: abjad.Container,
) -> None:
if not isinstance(contents, abjad.Container):
raise TypeError("'contents' must be 'abjad.Container' or child "
"class")
if not abjad.select(contents).leaves().are_contiguous_logical_voice():
raise ValueError("'contents' must be contiguous logical voice")
if isinstance(contents, abjad.Score):
self._contents = abjad.mutate.copy(contents[0])
elif isinstance(contents, abjad.Tuplet):
self._contents = abjad.Container([abjad.mutate.copy(contents)])
else:
self._contents = abjad.mutate.copy(contents)
dummy_container = abjad.mutate.copy(contents)
self._current_window = dummy_container[:]
dummy_container[:] = []
self._is_first_window = True
@property
def pitches(self) -> abjad.PitchSegment:
r'Pitches available for the randomiser.'
return self._pitches
@pitches.setter
def pitches(self,
pitches: Union[list[Union[int, float, str, abjad.Pitch]],
tuple[Union[int, float, str, abjad.Pitch]],
str,
abjad.PitchSegment,
],
) -> None:
if not isinstance(pitches, (list, tuple, str, abjad.PitchSegment)):
raise TypeError("'pitches' must be 'list', 'tuple', 'str', or "
"'abjad.PitchSegment'")
if isinstance(pitches, (list, tuple, str)):
self._pitches = abjad.PitchSegment(pitches)
else:
self._pitches = pitches
pitch_list = [pitch for pitch in self._pitches]
self._tenney_selector = TenneySelector(pitch_list)
if self._weights is not None:
if len(pitch_list) != len(self._weights):
self.weights = None
@property
def weights(self) -> list[Union[float, int]]:
r'The :obj:`list` with weights for each element of :attr:`pitches`'
return self._weights
@weights.setter
def weights(self,
weights: Optional[list[Union[float, int]]],
) -> None:
if weights is not None:
if not isinstance(weights, list):
raise TypeError("'weights' must be 'list'")
if not self.__len__() == len(weights):
raise ValueError("'weights' must have the same length as "
"'pitches'")
if not all(isinstance(weight, (int, float))
for weight in weights):
raise TypeError("'weights' elements must be 'int' or 'float'")
self._weights = weights[:]
else:
self._weights = None
self._tenney_selector.weights = self._weights
@property
def omit_time_signatures(self) -> bool:
r'When ``True``, the output will contain no time signatures.'
return self._omit_time_signatures
@omit_time_signatures.setter
def omit_time_signatures(self,
omit_time_signatures: bool,
) -> None:
if not isinstance(omit_time_signatures, bool):
raise TypeError("'omit_time_signatures' must be 'bool'")
self._omit_time_signatures = omit_time_signatures
@property
def process_on_first_call(self) -> bool:
r"""If ``True`` then :attr:`contents` will be processed in the very
first call.
"""
return self._process_on_first_call
@process_on_first_call.setter
def process_on_first_call(self,
process_on_first_call: bool,
) -> None:
if not isinstance(process_on_first_call, bool):
raise TypeError("'process_on_first_call' must be 'bool'")
self._process_on_first_call = process_on_first_call
@property
def use_tenney_selector(self) -> bool:
r"""If ``True`` then the pitches will be selected using
:class:`auxjad.TenneySelector`, otherwise they are chosen using a
uniform random distribution.
"""
return self._use_tenney_selector
@use_tenney_selector.setter
def use_tenney_selector(self,
use_tenney_selector: bool,
) -> None:
if not isinstance(use_tenney_selector, bool):
raise TypeError("'use_tenney_selector' must be 'bool'")
self._use_tenney_selector = use_tenney_selector
@property
def current_window(self) -> abjad.Selection:
r'Read-only property, returns the result of the last operation.'
current_window = abjad.mutate.copy(self._current_window)
if self._omit_time_signatures:
self._remove_all_time_signatures(current_window)
return current_window