Skip to content

xnano.music

xnano.music

xnano.music

Notes and sound: note makes a Note you can define anywhere, and play plays notes, sounds and melodies on the running canvas or on this computer's sound output. Playing needs the optional audio extra: pip install "xnano[audio]".

Classes:

  • Note –

    A note, or a chord, to play: a pitch, how long it lasts and how it sounds.

  • PlayHandle –

    What play returns: tells whether the sound is over and can cut it short.

Functions:

  • note –

    Make a Note: a pitch, or several for a chord, with its length and sound.

  • play –

    Play a note, a sound, a symbol name, a melody or a sequence of those.

Note dataclass

Note(pitch: Pitch | tuple[Pitch, ...], beats: float = 1.0, instrument: InstrumentName = 'piano', velocity: float = 0.8, effects: tuple[PlayableEffectSpecification, ...] = (), symbol: str | None = None)

A note, or a chord, to play: a pitch, how long it lasts and how it sounds.

A note is a plain value. It needs no sound device, no canvas and not even the audio extra to be created, so a song can be defined at the top of a module and played later with play or Canvas.play. A sequence of notes plays one after another. symbol registers the note under a name, so play('kick') finds it.

Attributes:

Raises:

  • TypeError –

    The pitch is not a note name or a number, or an effect is not a dictionary.

  • ValueError –

    The pitch is not valid, beats is not above 0, velocity is not from 0.0 to 1.0, or the instrument is unknown (checked when the audio extra is installed, and otherwise when the note is played).

Examples:

import xnano

chord = xnano.Note(('C4', 'E4', 'G4'), beats=2.0, instrument='bell')
assert chord.beats == 2.0
with xnano.new(width=8, height=1) as canvas:
    canvas.play([chord, xnano.note('C5')])
    canvas.step(delta_seconds=1.0)
    assert canvas.audio.render_sound().duration_milliseconds == 1000.0

Methods:

  • __post_init__ –

    Check the values and register the symbol.

  • __xnano_play__ –

    Return this note, so a note is playable like any other component.

pitch instance-attribute

pitch: Pitch | tuple[Pitch, ...]

A note name, a MIDI number, or a tuple of them for a chord.

beats class-attribute instance-attribute

beats: float = 1.0

How long the note is held, in beats.

instrument class-attribute instance-attribute

instrument: InstrumentName = 'piano'

The built-in instrument.

velocity class-attribute instance-attribute

velocity: float = 0.8

How hard the note is played, from 0.0 to 1.0.

effects class-attribute instance-attribute

effects: tuple[PlayableEffectSpecification, ...] = ()

The sound effects, as dictionaries with a kind key.

symbol class-attribute instance-attribute

symbol: str | None = None

The registered symbol name, or None.

__post_init__

__post_init__() -> None

Check the values and register the symbol.

Source code in xnano/music.py
def __post_init__(self) -> None:
    """Check the values and register the symbol."""
    pitch = self.pitch
    if isinstance(pitch, (list, tuple)):
        if not pitch:
            raise ValueError(
                'A chord needs at least one pitch, but got an empty one. Pass '
                'note(("C4", "E4", "G4")) for a chord or note("C4") for one note.'
            )
        pitch = tuple(pitch)
        for each in pitch:
            _check_pitch(each)
        object.__setattr__(self, 'pitch', pitch)
    else:
        _check_pitch(pitch)
    if (
        isinstance(self.beats, bool)
        or not isinstance(self.beats, (int, float))
        or not math.isfinite(self.beats)
        or self.beats <= 0
    ):
        raise ValueError(
            f'beats must be a number above 0, but was {self.beats!r}. Use 1.0 for one '
            f'beat or 0.5 for half a beat.'
        )
    if isinstance(self.velocity, bool) or not isinstance(self.velocity, (int, float)):
        raise ValueError(
            f'velocity must be a number from 0.0 to 1.0, but was {self.velocity!r}. Use '
            f'0.8 for an ordinary note.'
        )
    if not isinstance(self.instrument, str):
        raise ValueError(
            f'instrument must be the name of a built-in instrument, such as "piano", '
            f'but was {self.instrument!r}.'
        )
    if not 0.0 <= self.velocity <= 1.0:
        raise ValueError(
            f'velocity must be from 0.0 to 1.0, but was {self.velocity!r}. Use 0.8 for '
            f'an ordinary note.'
        )
    audio = _installed_audio()
    if audio is not None:
        names = audio.instrument_names()
        if self.instrument not in names:
            raise ValueError(
                f'{self.instrument!r} is not an instrument. Use one of: '
                f'{", ".join(names)}.'
            )
    effects = effect_sequence(self.effects, 'playable')
    for effect in effects:
        normalize_effect(effect, 'playable')
    object.__setattr__(self, 'effects', effects)
    if self.symbol is not None:
        symbols.register(self.symbol, self)

__xnano_play__

__xnano_play__(context: Context) -> Note

Return this note, so a note is playable like any other component.

Source code in xnano/music.py
def __xnano_play__(self, context: Context) -> Note:
    """Return this note, so a note is playable like any other component."""
    return self

PlayHandle

PlayHandle(*, clock: Callable[[], float], end_seconds: float, cancel: Callable[[], None] | None = None)

What play returns: tells whether the sound is over and can cut it short.

Attributes:

  • is_finished (bool) –

    Whether the sound has ended or the handle was removed. On a canvas from new this follows the canvas's clock; on a device it follows the real time since the sound was handed over.

Examples:

import xnano

with xnano.new(width=8, height=1) as canvas:
    handle = canvas.play(xnano.note('C4', beats=1.0))
    assert not handle.is_finished
    handle.remove()
    assert handle.is_finished

Make a handle. Use the handle that play returns instead of calling this.

Parameters:

  • clock (Callable[[], float]) –

    Returns the current time in seconds.

  • end_seconds (float) –

    The time on that clock at which the sound is over.

  • cancel (Callable[[], None] | None, default: None ) –

    Called once by remove().

Methods:

  • remove –

    Stop the sound from playing any further, as far as it has not been heard yet.

Source code in xnano/music.py
def __init__(
    self,
    *,
    clock: Callable[[], float],
    end_seconds: float,
    cancel: Callable[[], None] | None = None,
) -> None:
    """
    Make a handle. Use the handle that ``play`` returns instead of calling this.

    Args:
        clock: Returns the current time in seconds.
        end_seconds: The time on that clock at which the sound is over.
        cancel: Called once by ``remove()``.
    """
    self._clock = clock
    self._end_seconds = end_seconds
    self._cancel = cancel
    self._is_removed = False

is_finished property

is_finished: bool

Whether the sound ended or the handle was removed.

remove

remove() -> None

Stop the sound from playing any further, as far as it has not been heard yet.

On a canvas from new the part that has not been rendered by render_sound is dropped. On a device a sound that was already handed to the output plays to its end, and one still waiting for its beat never starts. Removing twice does nothing.

Source code in xnano/music.py
def remove(self) -> None:
    """
    Stop the sound from playing any further, as far as it has not been heard yet.

    On a canvas from [`new`][xnano.new] the part that has not been rendered by
    [`render_sound`][xnano.audio.AudioDevice.render_sound] is dropped. On a device a sound
    that was already handed to the output plays to its end, and one still waiting for its
    beat never starts. Removing twice does nothing.
    """
    if self._is_removed:
        return
    self._is_removed = True
    if self._cancel is not None:
        self._cancel()

note

note(pitch: Pitch | Sequence[Pitch], *, beats: float = 1.0, instrument: InstrumentName = 'piano', velocity: float = 0.8, effects: Sequence[PlayableEffectSpecification] = (), symbol: str | None = None) -> Note

Make a Note: a pitch, or several for a chord, with its length and sound.

Nothing is played and no sound device is needed. Play it with play or Canvas.play. symbol='kick' registers the note, and then play('kick') plays it.

Parameters:

  • pitch (Pitch | Sequence[Pitch]) –

    A note name such as 'C#4', a MIDI number from 0 to 127, or a sequence of them for a chord.

  • beats (float, default: 1.0 ) –

    How long the note is held, in beats.

  • instrument (InstrumentName, default: 'piano' ) –

    A built-in instrument, from instrument_names.

  • velocity (float, default: 0.8 ) –

    How hard the note is played, from 0.0 to 1.0.

  • effects (Sequence[PlayableEffectSpecification], default: () ) –

    Sound effects as dictionaries with a kind key, such as {'kind': 'audio.reverb'}.

  • symbol (str | None, default: None ) –

    A name to register the note under in symbols. A str given to play is looked up as a symbol name before it is read as a note name, so the name must not look like a note.

Raises:

  • TypeError –

    The pitch is not a note name or a number.

  • ValueError –

    A value is out of range or the instrument is unknown.

Examples:

import xnano

kick = xnano.note('C2', instrument='kick', symbol='kick_drum')
with xnano.new(width=8, height=1) as canvas:
    canvas.play('kick_drum')
    canvas.step(delta_seconds=0.5)
    assert canvas.audio.render_sound().peak > 0.0
Source code in xnano/music.py
def note(
    pitch: Pitch | Sequence[Pitch],
    *,
    beats: float = 1.0,
    instrument: InstrumentName = 'piano',
    velocity: float = 0.8,
    effects: Sequence[PlayableEffectSpecification] = (),
    symbol: str | None = None,
) -> Note:
    """
    Make a [`Note`][xnano.Note]: a pitch, or several for a chord, with its length and sound.

    Nothing is played and no sound device is needed. Play it with [`play`][xnano.play] or
    [`Canvas.play`][xnano.canvas.Canvas.play]. ``symbol='kick'`` registers the note, and then
    ``play('kick')`` plays it.

    Args:
        pitch: A note name such as ``'C#4'``, a MIDI number from 0 to 127, or a sequence of
            them for a chord.
        beats: How long the note is held, in beats.
        instrument: A built-in instrument, from
            [`instrument_names`][xnano_audio.instrument_names].
        velocity: How hard the note is played, from 0.0 to 1.0.
        effects: Sound effects as dictionaries with a ``kind`` key, such as
            ``{'kind': 'audio.reverb'}``.
        symbol: A name to register the note under in [`symbols`][xnano.symbols]. A ``str``
            given to [`play`][xnano.play] is looked up as a symbol name before it is read as a
            note name, so the name must not look like a note.

    Raises:
        TypeError: The pitch is not a note name or a number.
        ValueError: A value is out of range or the instrument is unknown.

    Examples:
        ```python
        import xnano

        kick = xnano.note('C2', instrument='kick', symbol='kick_drum')
        with xnano.new(width=8, height=1) as canvas:
            canvas.play('kick_drum')
            canvas.step(delta_seconds=0.5)
            assert canvas.audio.render_sound().peak > 0.0
        ```
    """
    chord = (
        tuple(pitch)
        if isinstance(pitch, Sequence) and not isinstance(pitch, str)
        else pitch
    )
    return Note(
        chord,  # type: ignore[arg-type]
        beats=beats,
        instrument=instrument,
        velocity=velocity,
        effects=effect_sequence(effects, 'playable'),  # type: ignore[arg-type]
        symbol=symbol,
    )

play

play(playable: PlaySource, /, **options: Unpack[PlayOptions]) -> PlayHandle

Play a note, a sound, a symbol name, a melody or a sequence of those.

The sound goes to the running canvas (see active); with no canvas running it goes to this computer's sound output. On a canvas from new, which has a clock you move, the sound is recorded and makes no noise: read it with canvas.audio.render_sound().

A str is a symbol name first, so play('kick') plays the note or sound registered with symbol='kick'. Any other str is a note name such as 'C#4', a frequency such as '440hz' or melody text: notes separated by spaces, :2 holds a note for two beats, rest is a silence, + joins a chord and | marks a bar, as in 'C4 E4 G4:2 rest C5+E5+G5'. A sequence plays one after another. An option replaces the same value on a Note, so play(note('C4'), instrument='violin') plays the violin.

Parameters:

  • playable (PlaySource) –

    What to play.

  • **options (Unpack[PlayOptions], default: {} ) –

    See PlayOptions: instrument, velocity, effects, volume, pan, at_beat and beats, the length in beats of a note name, a number or a Note.

Returns:

Raises:

  • ImportError –

    xnano-audio is not installed. Install it with pip install "xnano[audio]".

  • TerminalAudioUnavailableError –

    No canvas is running and this computer has no sound output device.

  • ValueError –

    An option is out of range, or a str is neither a symbol, a note name nor melody text. The message lists the known sound symbols.

Examples:

import xnano

with xnano.new(width=8, height=1) as canvas:
    xnano.play([xnano.note('C4'), xnano.note('E4'), xnano.note('G4')])
    canvas.step(delta_seconds=1.5)
    assert canvas.audio.render_sound().peak > 0.0
Source code in xnano/music.py
def play(playable: PlaySource, /, **options: Unpack[PlayOptions]) -> PlayHandle:
    """
    Play a note, a sound, a symbol name, a melody or a sequence of those.

    The sound goes to the running canvas (see [`active`][xnano.active]); with no canvas
    running it goes to this computer's sound output. On a canvas from [`new`][xnano.new],
    which has a clock you move, the sound is recorded and makes no noise: read it with
    ``canvas.audio.render_sound()``.

    A ``str`` is a symbol name first, so ``play('kick')`` plays the note or sound registered
    with ``symbol='kick'``. Any other ``str`` is a note name such as ``'C#4'``, a frequency such
    as ``'440hz'`` or melody text: notes separated by spaces, ``:2`` holds a note for two
    beats, ``rest`` is a silence, ``+`` joins a chord and ``|`` marks a bar, as in
    ``'C4 E4 G4:2 rest C5+E5+G5'``. A sequence plays one after another. An option replaces
    the same value on a [`Note`][xnano.Note], so ``play(note('C4'), instrument='violin')``
    plays the violin.

    Args:
        playable: What to play.
        **options: See [`PlayOptions`][xnano.types.PlayOptions]: ``instrument``, ``velocity``,
            ``effects``, ``volume``, ``pan``, ``at_beat`` and ``beats``, the length in beats
            of a note name, a number or a ``Note``.

    Returns:
        A [`PlayHandle`][xnano.music.PlayHandle].

    Raises:
        ImportError: ``xnano-audio`` is not installed. Install it with
            ``pip install "xnano[audio]"``.
        TerminalAudioUnavailableError: No canvas is running and this computer has no sound
            output device.
        ValueError: An option is out of range, or a ``str`` is neither a symbol, a note name
            nor melody text. The message lists the known sound symbols.

    Examples:
        ```python
        import xnano

        with xnano.new(width=8, height=1) as canvas:
            xnano.play([xnano.note('C4'), xnano.note('E4'), xnano.note('G4')])
            canvas.step(delta_seconds=1.5)
            assert canvas.audio.render_sound().peak > 0.0
        ```
    """
    from xnano.canvas import active

    canvas = active(is_required=False)
    if canvas is not None:
        return canvas.play(playable, **options)
    return _default_player().play(playable, **options)