Skip to content

xnano.audio

xnano.audio

xnano.audio

xnano-audio based components and utilities for sound playback and composition. Needs the optional audio extra: pip install "xnano[audio]".

Classes:

Functions:

Attributes:

InstrumentName module-attribute

InstrumentName: TypeAlias = Literal['piano', 'electric_piano', 'organ', 'bell', 'marimba', 'guitar', 'electric_guitar', 'pluck', 'bass', 'synth', 'pad', 'strings', 'sine', 'square', 'saw', 'triangle', 'kick', 'snare', 'hihat', 'open_hat', 'clap', 'tom', 'cowbell']

NoteName module-attribute

NoteName: TypeAlias = Literal['C0', 'C#0', 'Db0', 'D0', 'D#0', 'Eb0', 'E0', 'F0', 'F#0', 'Gb0', 'G0', 'G#0', 'Ab0', 'A0', 'A#0', 'Bb0', 'B0', 'C1', 'C#1', 'Db1', 'D1', 'D#1', 'Eb1', 'E1', 'F1', 'F#1', 'Gb1', 'G1', 'G#1', 'Ab1', 'A1', 'A#1', 'Bb1', 'B1', 'C2', 'C#2', 'Db2', 'D2', 'D#2', 'Eb2', 'E2', 'F2', 'F#2', 'Gb2', 'G2', 'G#2', 'Ab2', 'A2', 'A#2', 'Bb2', 'B2', 'C3', 'C#3', 'Db3', 'D3', 'D#3', 'Eb3', 'E3', 'F3', 'F#3', 'Gb3', 'G3', 'G#3', 'Ab3', 'A3', 'A#3', 'Bb3', 'B3', 'C4', 'C#4', 'Db4', 'D4', 'D#4', 'Eb4', 'E4', 'F4', 'F#4', 'Gb4', 'G4', 'G#4', 'Ab4', 'A4', 'A#4', 'Bb4', 'B4', 'C5', 'C#5', 'Db5', 'D5', 'D#5', 'Eb5', 'E5', 'F5', 'F#5', 'Gb5', 'G5', 'G#5', 'Ab5', 'A5', 'A#5', 'Bb5', 'B5', 'C6', 'C#6', 'Db6', 'D6', 'D#6', 'Eb6', 'E6', 'F6', 'F#6', 'Gb6', 'G6', 'G#6', 'Ab6', 'A6', 'A#6', 'Bb6', 'B6', 'C7', 'C#7', 'Db7', 'D7', 'D#7', 'Eb7', 'E7', 'F7', 'F#7', 'Gb7', 'G7', 'G#7', 'Ab7', 'A7', 'A#7', 'Bb7', 'B7', 'C8', 'C#8', 'Db8', 'D8', 'D#8', 'Eb8', 'E8', 'F8', 'F#8', 'Gb8', 'G8', 'G#8', 'Ab8', 'A8', 'A#8', 'Bb8', 'B8']

PlaySource module-attribute

PlaySource: TypeAlias = 'Playable | int | float | TerminalSound | TerminalNote | TerminalMelody'

Anything play and Canvas.play accept: a Playable (a Note, an Audio, a symbol name, a note name such as 'C#4', melody text, or a sequence of those), a MIDI note number such as 60, a frequency in hertz, or a rendered sound, note or melody from xnano-audio.

Audio dataclass

Audio(source: TerminalSound | PathLike[str] | str | bytes, *, symbol: str | None = None)

A sound you can play with Canvas.play or AudioDevice.play.

Make one from the path of a WAV file, the bytes of one, or a TerminalSound.

Attributes:

  • sound (TerminalSound) –

    The rendered sound.

  • symbol (str | None) –

    The name the sound is registered under in symbols, or None. play('kick') plays the sound registered as 'kick'.

Examples:

import xnano
from xnano_audio import TerminalSound

audio = xnano.Audio(TerminalSound.from_samples([0.0, 0.5, -0.5, 0.0]))
assert audio.sound.sample_rate == 44100

Load a sound.

Parameters:

  • source (TerminalSound | PathLike[str] | str | bytes) –

    The path of a WAV file, the bytes of one, or a sound.

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

    A name to register the sound under in symbols, so play(name) plays it.

Raises:

  • ImportError –

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

  • TypeError –

    source is not a path, bytes or a sound.

  • FileNotFoundError –

    source is a path to a file that does not exist.

  • ValueError –

    The file is not a PCM WAV file that xnano can read, or symbol cannot be a symbol name, or another kind of value already has it.

Source code in xnano/_media.py
def __init__(
    self,
    source: TerminalSound | os.PathLike[str] | str | bytes,
    *,
    symbol: str | None = None,
) -> None:
    """
    Load a sound.

    Args:
        source: The path of a WAV file, the bytes of one, or a sound.
        symbol: A name to register the sound under in [`symbols`][xnano.symbols], so
            ``play(name)`` plays it.

    Raises:
        ImportError: ``xnano-audio`` is not installed. Install it with
            ``pip install "xnano[audio]"``.
        TypeError: ``source`` is not a path, bytes or a sound.
        FileNotFoundError: ``source`` is a path to a file that does not exist.
        ValueError: The file is not a PCM WAV file that xnano can read, or ``symbol`` cannot
            be a symbol name, or another kind of value already has it.
    """
    module = require('audio', needed_for='xnano.Audio')
    if isinstance(source, module.TerminalSound):
        sound = source
    elif isinstance(source, bytes):
        sound = _decode_wave(module, source)
    elif isinstance(source, (str, os.PathLike)):
        path = pathlib.Path(source)
        if not path.is_file():
            raise FileNotFoundError(
                f'Audio could not load {str(path)!r}, because no WAV file exists '
                f'there. Check the path, or pass the bytes of the sound.'
            )
        sound = _decode_wave(module, path.read_bytes())
    else:
        raise TypeError(
            f'Audio needs the path of a WAV file, the bytes of one or a sound, but got '
            f'{type(source).__name__}. Pass Audio("sound.wav").'
        )
    object.__setattr__(self, 'sound', sound)
    object.__setattr__(self, 'symbol', symbol)
    if symbol is not None:
        symbols.register(symbol, self)

TerminalAudioEffect

An immutable audio effect description.

Build one with a static constructor and pass it, in order, to render_note, TerminalSound.with_effects or TerminalAudioPlayer.play_note. Effects hold no state, so one instance can be reused for any number of sounds.

Every constructor raises ValueError when an argument is out of range.

Attributes:

  • name (str) –

    The effect's kind, such as "reverb".

Methods:

  • bit_crush –

    Lo-fi: quantize to bits and hold every downsample-th sample.

  • distortion –

    Soft-clip (tanh) distortion.

  • low_pass –

    Remove frequencies above cutoff.

  • high_pass –

    Remove frequencies below cutoff.

  • delay –

    Echoes.

  • reverb –

    Room reverb (Freeverb-style).

  • tremolo –

    Amplitude wobble.

  • __or__ –

    Chain this effect with another effect or sequence of effects.

name property

name: str

The effect's kind, such as "reverb".

bit_crush staticmethod

bit_crush(*, bits: int = 8, downsample: int = 4) -> TerminalAudioEffect

Lo-fi: quantize to bits and hold every downsample-th sample.

Parameters:

  • bits (int, default: 8 ) –

    Resolution, 1 to 16. Lower is crunchier.

  • downsample (int, default: 4 ) –

    Hold each value for this many samples, 1 to 256.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def bit_crush(*, bits: int = 8, downsample: int = 4) -> TerminalAudioEffect:
    """
    Lo-fi: quantize to ``bits`` and hold every ``downsample``-th sample.

    Args:
        bits: Resolution, 1 to 16. Lower is crunchier.
        downsample: Hold each value for this many samples, 1 to 256.
    """

distortion staticmethod

distortion(*, drive: float = 4.0) -> TerminalAudioEffect

Soft-clip (tanh) distortion.

Parameters:

  • drive (float, default: 4.0 ) –

    Input gain before clipping, 0.1 to 100. Around 1 is nearly clean; higher is harsher. The output is normalized.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def distortion(*, drive: float = 4.0) -> TerminalAudioEffect:
    """
    Soft-clip (tanh) distortion.

    Args:
        drive: Input gain before clipping, 0.1 to 100. Around 1 is nearly
            clean; higher is harsher. The output is normalized.
    """

low_pass staticmethod

low_pass(*, cutoff: float = 1200.0, resonance: float = 0.707) -> TerminalAudioEffect

Remove frequencies above cutoff.

Parameters:

  • cutoff (float, default: 1200.0 ) –

    Corner frequency in hertz, 20 to 20000.

  • resonance (float, default: 0.707 ) –

    Filter Q, 0.1 to 20. The default is flat.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def low_pass(
    *, cutoff: float = 1200.0, resonance: float = 0.707
) -> TerminalAudioEffect:
    """
    Remove frequencies above ``cutoff``.

    Args:
        cutoff: Corner frequency in hertz, 20 to 20000.
        resonance: Filter Q, 0.1 to 20. The default is flat.
    """

high_pass staticmethod

high_pass(*, cutoff: float = 400.0, resonance: float = 0.707) -> TerminalAudioEffect

Remove frequencies below cutoff.

Parameters:

  • cutoff (float, default: 400.0 ) –

    Corner frequency in hertz, 20 to 20000.

  • resonance (float, default: 0.707 ) –

    Filter Q, 0.1 to 20. The default is flat.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def high_pass(
    *, cutoff: float = 400.0, resonance: float = 0.707
) -> TerminalAudioEffect:
    """
    Remove frequencies below ``cutoff``.

    Args:
        cutoff: Corner frequency in hertz, 20 to 20000.
        resonance: Filter Q, 0.1 to 20. The default is flat.
    """

delay staticmethod

delay(*, time_milliseconds: float = 250.0, feedback: float = 0.4, mix: float = 0.35) -> TerminalAudioEffect

Echoes.

The result is longer than the input so the echoes ring out.

Parameters:

  • time_milliseconds (float, default: 250.0 ) –

    Time between echoes, 1 to 2000.

  • feedback (float, default: 0.4 ) –

    How much of each echo is fed back, 0.0 to 0.95.

  • mix (float, default: 0.35 ) –

    Level of the echoes against the dry sound, 0.0 to 1.0.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def delay(
    *, time_milliseconds: float = 250.0, feedback: float = 0.4, mix: float = 0.35
) -> TerminalAudioEffect:
    """
    Echoes.

    The result is longer than the input so the echoes ring out.

    Args:
        time_milliseconds: Time between echoes, 1 to 2000.
        feedback: How much of each echo is fed back, 0.0 to 0.95.
        mix: Level of the echoes against the dry sound, 0.0 to 1.0.
    """

reverb staticmethod

reverb(*, room: float = 0.5, damping: float = 0.5, mix: float = 0.25) -> TerminalAudioEffect

Room reverb (Freeverb-style).

The result is longer than the input so the tail rings out.

Parameters:

  • room (float, default: 0.5 ) –

    Room size, 0.0 to 1.0. Larger rooms ring longer.

  • damping (float, default: 0.5 ) –

    How quickly high frequencies fade, 0.0 to 1.0.

  • mix (float, default: 0.25 ) –

    Wet level against the dry sound, 0.0 to 1.0.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def reverb(
    *, room: float = 0.5, damping: float = 0.5, mix: float = 0.25
) -> TerminalAudioEffect:
    """
    Room reverb (Freeverb-style).

    The result is longer than the input so the tail rings out.

    Args:
        room: Room size, 0.0 to 1.0. Larger rooms ring longer.
        damping: How quickly high frequencies fade, 0.0 to 1.0.
        mix: Wet level against the dry sound, 0.0 to 1.0.
    """

tremolo staticmethod

tremolo(*, rate_hertz: float = 5.0, depth: float = 0.5) -> TerminalAudioEffect

Amplitude wobble.

Parameters:

  • rate_hertz (float, default: 5.0 ) –

    Wobbles per second, 0.1 to 40.

  • depth (float, default: 0.5 ) –

    0.0 (none) to 1.0 (silent at each trough).

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def tremolo(*, rate_hertz: float = 5.0, depth: float = 0.5) -> TerminalAudioEffect:
    """
    Amplitude wobble.

    Args:
        rate_hertz: Wobbles per second, 0.1 to 40.
        depth: 0.0 (none) to 1.0 (silent at each trough).
    """

__or__

__or__(other: AudioEffectSource) -> list[TerminalAudioEffect]

Chain this effect with another effect or sequence of effects.

Examples:

chain = TerminalAudioEffect.low_pass(cutoff=800.0) | TerminalAudioEffect.reverb(room=0.5)
assert [effect.name for effect in chain] == ["low_pass", "reverb"]
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __or__(self, other: AudioEffectSource) -> list[TerminalAudioEffect]:
    """
    Chain this effect with another effect or sequence of effects.

    Examples:
        ```python
        chain = TerminalAudioEffect.low_pass(cutoff=800.0) | TerminalAudioEffect.reverb(room=0.5)
        assert [effect.name for effect in chain] == ["low_pass", "reverb"]
        ```
    """

TerminalAudioError

Bases: RuntimeError

Raised when an audio device or stream fails.

Treat this (and a failed import of the package) as "audio is not available": SSH sessions, containers and CI usually have no audio device.

TerminalAudioUnavailableError

Bases: TerminalAudioError

Raised when an audio device is missing, or this build has no device support.

The message says which of the two it is and how to fix it.

TerminalMelody

TerminalMelody(notes: list[TerminalNote] | None = None, *, tempo_map: TerminalTempoMap | None = None, time_signature: TerminalTimeSignature | None = None, length_beats: float = 0.0)

Notes on a beat timeline with a tempo map and a time signature.

Immutable: every method that changes the melody returns a new one. Notes can start together to form chords. Ask which notes are due between two readings of any clock with scheduled_between, or render the melody to a TerminalSound with render. len(melody) is the number of notes and iterating yields the notes in start order.

Attributes:

Examples:

melody = (
    TerminalMelody(tempo_map=TerminalTempoMap(100))
    .appended_note("C4")
    .appended_chord(["E4", "G4"], duration_beats=2)
    .appended_rest(1)
)
sound = melody.render("piano")
due = melody.scheduled_between(0.0, 1.0)

Create a melody.

Parameters:

  • notes (list[TerminalNote] | None, default: None ) –

    The notes, in any order.

  • tempo_map (TerminalTempoMap | None, default: None ) –

    Defaults to a constant 120 beats per minute.

  • time_signature (TerminalTimeSignature | None, default: None ) –

    Defaults to 4/4.

  • length_beats (float, default: 0.0 ) –

    A minimum length that adds trailing silence after the last note.

Raises:

  • ValueError –

    If there are more than 100000 notes or length_beats is negative.

Methods:

  • with_notes –

    Return the melody with notes added.

  • with_tempo_map –

    Return the melody with another tempo map.

  • with_time_signature –

    Return the melody with another time signature.

  • with_length_beats –

    Return the melody lasting at least length_beats, which adds trailing silence.

  • appended_note –

    Return the melody with one note appended at its end.

  • appended_chord –

    Return the melody with notes that sound together appended at its end.

  • appended_rest –

    Return the melody with silence appended at its end.

  • transposed –

    Return the melody with every pitch moved by semitones.

  • shifted –

    Return the melody moved later by beats, or earlier when negative.

  • stretched –

    Return the melody with every start, duration and tempo change multiplied by factor.

  • repeated –

    Return the melody played count times in a row.

  • followed_by –

    Return the melody followed by other, keeping this melody's tempo map.

  • layered_with –

    Return the notes of both melodies played together, keeping this tempo map.

  • __add__ –

    Play something after the melody, or raise it by semitones.

  • __sub__ –

    Lower every note by semitones.

  • __mul__ –

    Play the melody several times in a row.

  • __or__ –

    Play two melodies together.

  • __rshift__ –

    Start the melody later by beats.

  • __lshift__ –

    Start the melody earlier by beats.

  • notes_between_beats –

    Return the notes that start in [from_beats, to_beats).

  • notes_between_seconds –

    Return the notes that start in [from_seconds, to_seconds) of melody time.

  • scheduled_between –

    Return the notes due on an external clock between two readings.

  • render –

    Render the melody, or the notes starting inside a window, to a sound.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __init__(
    self,
    notes: list[TerminalNote] | None = None,
    *,
    tempo_map: TerminalTempoMap | None = None,
    time_signature: TerminalTimeSignature | None = None,
    length_beats: float = 0.0,
) -> None:
    """
    Create a melody.

    Args:
        notes: The notes, in any order.
        tempo_map: Defaults to a constant 120 beats per minute.
        time_signature: Defaults to 4/4.
        length_beats: A minimum length that adds trailing silence after the last note.

    Raises:
        ValueError: If there are more than 100000 notes or ``length_beats`` is negative.
    """

notes property

The notes in start order.

tempo_map property

tempo_map: TerminalTempoMap

The tempo map.

time_signature property

time_signature: TerminalTimeSignature

The time signature.

length_beats property

length_beats: float

The end of the last note, or the explicit length when that is longer, in beats.

duration_seconds property

duration_seconds: float

The melody's length in seconds.

with_notes

with_notes(notes: list[TerminalNote]) -> TerminalMelody

Return the melody with notes added.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_notes(self, notes: list[TerminalNote]) -> TerminalMelody:
    """Return the melody with ``notes`` added."""

with_tempo_map

with_tempo_map(tempo_map: TerminalTempoMap) -> TerminalMelody

Return the melody with another tempo map.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_tempo_map(self, tempo_map: TerminalTempoMap) -> TerminalMelody:
    """Return the melody with another tempo map."""

with_time_signature

with_time_signature(time_signature: TerminalTimeSignature) -> TerminalMelody

Return the melody with another time signature.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_time_signature(
    self, time_signature: TerminalTimeSignature
) -> TerminalMelody:
    """Return the melody with another time signature."""

with_length_beats

with_length_beats(length_beats: float) -> TerminalMelody

Return the melody lasting at least length_beats, which adds trailing silence.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_length_beats(self, length_beats: float) -> TerminalMelody:
    """Return the melody lasting at least ``length_beats``, which adds trailing silence."""

appended_note

appended_note(pitch: Pitch, *, duration_beats: float = 1.0, velocity: float = 0.8) -> TerminalMelody

Return the melody with one note appended at its end.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def appended_note(
    self, pitch: Pitch, *, duration_beats: float = 1.0, velocity: float = 0.8
) -> TerminalMelody:
    """Return the melody with one note appended at its end."""

appended_chord

appended_chord(pitches: Sequence[Pitch], *, duration_beats: float = 1.0, velocity: float = 0.8) -> TerminalMelody

Return the melody with notes that sound together appended at its end.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def appended_chord(
    self,
    pitches: Sequence[Pitch],
    *,
    duration_beats: float = 1.0,
    velocity: float = 0.8,
) -> TerminalMelody:
    """Return the melody with notes that sound together appended at its end."""

appended_rest

appended_rest(duration_beats: float = 1.0) -> TerminalMelody

Return the melody with silence appended at its end.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def appended_rest(self, duration_beats: float = 1.0) -> TerminalMelody:
    """Return the melody with silence appended at its end."""

transposed

transposed(semitones: int) -> TerminalMelody

Return the melody with every pitch moved by semitones.

Raises:

  • ValueError –

    If a pitch would leave the MIDI range 0 to 127.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def transposed(self, semitones: int) -> TerminalMelody:
    """
    Return the melody with every pitch moved by ``semitones``.

    Raises:
        ValueError: If a pitch would leave the MIDI range 0 to 127.
    """

shifted

shifted(beats: float) -> TerminalMelody

Return the melody moved later by beats, or earlier when negative.

Raises:

  • ValueError –

    If a note would start before beat zero.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def shifted(self, beats: float) -> TerminalMelody:
    """
    Return the melody moved later by ``beats``, or earlier when negative.

    Raises:
        ValueError: If a note would start before beat zero.
    """

stretched

stretched(factor: float) -> TerminalMelody

Return the melody with every start, duration and tempo change multiplied by factor.

The tempo values are unchanged, so 2.0 makes the melody twice as long in time.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def stretched(self, factor: float) -> TerminalMelody:
    """
    Return the melody with every start, duration and tempo change multiplied by ``factor``.

    The tempo values are unchanged, so 2.0 makes the melody twice as long in time.
    """

repeated

repeated(count: int) -> TerminalMelody

Return the melody played count times in a row.

Raises:

  • ValueError –

    If count is zero or the result would hold too many notes.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def repeated(self, count: int) -> TerminalMelody:
    """
    Return the melody played ``count`` times in a row.

    Raises:
        ValueError: If ``count`` is zero or the result would hold too many notes.
    """

followed_by

followed_by(other: TerminalMelody) -> TerminalMelody

Return the melody followed by other, keeping this melody's tempo map.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def followed_by(self, other: TerminalMelody) -> TerminalMelody:
    """Return the melody followed by ``other``, keeping this melody's tempo map."""

layered_with

layered_with(other: TerminalMelody) -> TerminalMelody

Return the notes of both melodies played together, keeping this tempo map.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def layered_with(self, other: TerminalMelody) -> TerminalMelody:
    """Return the notes of both melodies played together, keeping this tempo map."""

__add__

__add__(other: int) -> TerminalMelody

Play something after the melody, or raise it by semitones.

Examples:

phrase = TerminalNote("C4") + TerminalNote("E4")
assert len((phrase + TerminalNote("G4")).notes) == 3
assert [note.name for note in phrase + 2] == ["D4", "F#4"]
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __add__(self, other: int | TerminalMelody | TerminalNote) -> TerminalMelody:
    """
    Play something after the melody, or raise it by semitones.

    Examples:
        ```python
        phrase = TerminalNote("C4") + TerminalNote("E4")
        assert len((phrase + TerminalNote("G4")).notes) == 3
        assert [note.name for note in phrase + 2] == ["D4", "F#4"]
        ```
    """

__sub__

__sub__(other: int) -> TerminalMelody

Lower every note by semitones.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __sub__(self, other: int) -> TerminalMelody:
    """Lower every note by semitones."""

__mul__

__mul__(other: int) -> TerminalMelody

Play the melody several times in a row.

Examples:

phrase = TerminalNote("C4") + TerminalNote("E4")
assert len((phrase * 3).notes) == 6
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __mul__(self, other: int) -> TerminalMelody:
    """
    Play the melody several times in a row.

    Examples:
        ```python
        phrase = TerminalNote("C4") + TerminalNote("E4")
        assert len((phrase * 3).notes) == 6
        ```
    """

__or__

Play two melodies together.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __or__(self, other: TerminalMelody | TerminalNote) -> TerminalMelody:
    """Play two melodies together."""

__rshift__

__rshift__(other: float) -> TerminalMelody

Start the melody later by beats.

Examples:

phrase = TerminalNote("C4") + TerminalNote("E4")
assert [note.start_beats for note in phrase >> 2] == [2.0, 3.0]
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __rshift__(self, other: float) -> TerminalMelody:
    """
    Start the melody later by beats.

    Examples:
        ```python
        phrase = TerminalNote("C4") + TerminalNote("E4")
        assert [note.start_beats for note in phrase >> 2] == [2.0, 3.0]
        ```
    """

__lshift__

__lshift__(other: float) -> TerminalMelody

Start the melody earlier by beats.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __lshift__(self, other: float) -> TerminalMelody:
    """Start the melody earlier by beats."""

notes_between_beats

notes_between_beats(from_beats: float, to_beats: float) -> list[TerminalNote]

Return the notes that start in [from_beats, to_beats).

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def notes_between_beats(
    self, from_beats: float, to_beats: float
) -> list[TerminalNote]:
    """Return the notes that start in ``[from_beats, to_beats)``."""

notes_between_seconds

notes_between_seconds(from_seconds: float, to_seconds: float) -> list[TerminalNote]

Return the notes that start in [from_seconds, to_seconds) of melody time.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def notes_between_seconds(
    self, from_seconds: float, to_seconds: float
) -> list[TerminalNote]:
    """Return the notes that start in ``[from_seconds, to_seconds)`` of melody time."""

scheduled_between

scheduled_between(from_elapsed_seconds: float, to_elapsed_seconds: float, *, start_seconds: float = 0.0, should_loop: bool = False) -> list[TerminalScheduledNote]

Return the notes due on an external clock between two readings.

The melody begins when the clock reads start_seconds. Notes that start in [from_elapsed_seconds, to_elapsed_seconds) are returned with their times on that same clock, so consecutive windows never repeat or skip a note. Call it every frame with the previous and current reading of a manual or monotonic clock.

Parameters:

  • from_elapsed_seconds (float) –

    The previous clock reading (inclusive).

  • to_elapsed_seconds (float) –

    The current clock reading (exclusive).

  • start_seconds (float, default: 0.0 ) –

    The clock reading at which the melody begins.

  • should_loop (bool, default: False ) –

    Restart the melody every time it reaches its length.

Raises:

  • ValueError –

    If a reading is not finite or to_elapsed_seconds is earlier than from_elapsed_seconds.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def scheduled_between(
    self,
    from_elapsed_seconds: float,
    to_elapsed_seconds: float,
    *,
    start_seconds: float = 0.0,
    should_loop: bool = False,
) -> list[TerminalScheduledNote]:
    """
    Return the notes due on an external clock between two readings.

    The melody begins when the clock reads ``start_seconds``. Notes that start in
    ``[from_elapsed_seconds, to_elapsed_seconds)`` are returned with their times on
    that same clock, so consecutive windows never repeat or skip a note. Call it every
    frame with the previous and current reading of a manual or monotonic clock.

    Args:
        from_elapsed_seconds: The previous clock reading (inclusive).
        to_elapsed_seconds: The current clock reading (exclusive).
        start_seconds: The clock reading at which the melody begins.
        should_loop: Restart the melody every time it reaches its length.

    Raises:
        ValueError: If a reading is not finite or ``to_elapsed_seconds`` is earlier than
            ``from_elapsed_seconds``.
    """

render

render(instrument: InstrumentName = 'piano', *, sample_rate: int = 44100, effects: list[TerminalAudioEffect] | None = None, from_seconds: float = 0.0, to_seconds: float | None = None) -> TerminalSound

Render the melody, or the notes starting inside a window, to a sound.

Notes are placed sample-accurately from the tempo map and mixed; the result is limited to stay below clipping. The first sample is the instant from_seconds. Notes ring past the window with their natural tail.

Parameters:

  • instrument (InstrumentName, default: 'piano' ) –
  • sample_rate (int, default: 44100 ) –

    Output rate in hertz, 8000 to 192000.

  • effects (list[TerminalAudioEffect] | None, default: None ) –

    Effects applied in order to the mixed result (at most 32).

  • from_seconds (float, default: 0.0 ) –

    Melody time of the first sample.

  • to_seconds (float | None, default: None ) –

    Melody time at which the window ends; defaults to the melody's end.

Raises:

  • ValueError –

    If the instrument is unknown, an argument is out of range or the window is longer than 600 seconds.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def render(
    self,
    instrument: InstrumentName = 'piano',
    *,
    sample_rate: int = 44100,
    effects: list[TerminalAudioEffect] | None = None,
    from_seconds: float = 0.0,
    to_seconds: float | None = None,
) -> TerminalSound:
    """
    Render the melody, or the notes starting inside a window, to a sound.

    Notes are placed sample-accurately from the tempo map and mixed; the result is
    limited to stay below clipping. The first sample is the instant ``from_seconds``.
    Notes ring past the window with their natural tail.

    Args:
        instrument: One of [`instrument_names`][xnano_audio.instrument_names].
        sample_rate: Output rate in hertz, 8000 to 192000.
        effects: Effects applied in order to the mixed result (at most 32).
        from_seconds: Melody time of the first sample.
        to_seconds: Melody time at which the window ends; defaults to the melody's end.

    Raises:
        ValueError: If the instrument is unknown, an argument is out of range or the
            window is longer than 600 seconds.
    """

TerminalNote

TerminalNote(pitch: Pitch, *, start_beats: float = 0.0, duration_beats: float = 1.0, velocity: float = 0.8)

One note on a beat timeline.

Immutable. Pitches are MIDI numbers (60 is middle C); a name such as "C#4" is accepted wherever a pitch is. Times are measured in quarter-note beats from the start of the melody. Every with_* method returns a new note.

Attributes:

Examples:

note = TerminalNote("C4", start_beats=1.0, duration_beats=0.5)
assert note.pitch == 60 and note.end_beats == 1.5

Create a note.

Parameters:

  • pitch (Pitch) –

    A NoteName such as "C4" or "Db5", or a MIDI number from 0 to 127. Lower case and a missing octave ("c#" is "C#4") are accepted too.

  • start_beats (float, default: 0.0 ) –

    Where the note starts, zero or more.

  • duration_beats (float, default: 1.0 ) –

    How long the key is held, greater than zero.

  • velocity (float, default: 0.8 ) –

    Strength from 0.0 to 1.0.

Raises:

  • ValueError –

    If an argument is out of range or the pitch name is not recognized.

  • TypeError –

    If pitch is not an int or str.

Methods:

  • with_pitch –

    Return the note with another pitch.

  • with_start_beats –

    Return the note starting at another beat.

  • with_duration_beats –

    Return the note held for another number of beats.

  • with_velocity –

    Return the note with another strength.

  • transposed –

    Return the note moved by semitones.

  • __add__ –

    Move the note up by semitones, or play something after it.

  • __sub__ –

    Move the note down by semitones, or measure the distance to another note.

  • __or__ –

    Play notes together, as a chord.

  • __int__ –

    The MIDI pitch, so int(TerminalNote("C4")) is 60.

  • __lt__ –

    Notes are ordered by pitch, then by where they start.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __init__(
    self,
    pitch: Pitch,
    *,
    start_beats: float = 0.0,
    duration_beats: float = 1.0,
    velocity: float = 0.8,
) -> None:
    """
    Create a note.

    Args:
        pitch: A [`NoteName`][xnano_audio.NoteName] such as ``"C4"`` or ``"Db5"``, or a
            MIDI number from 0 to 127. Lower case and a missing octave (``"c#"`` is
            ``"C#4"``) are accepted too.
        start_beats: Where the note starts, zero or more.
        duration_beats: How long the key is held, greater than zero.
        velocity: Strength from 0.0 to 1.0.

    Raises:
        ValueError: If an argument is out of range or the pitch name is not recognized.
        TypeError: If ``pitch`` is not an ``int`` or ``str``.
    """

pitch property

pitch: int

The MIDI pitch, 0 to 127.

name property

name: NoteName

The sharp-spelled note name, such as "C#4".

frequency_hertz property

frequency_hertz: float

The pitch in hertz.

velocity property

velocity: float

The strength, 0.0 to 1.0.

start_beats property

start_beats: float

Where the note starts, in beats.

duration_beats property

duration_beats: float

How long the key is held, in beats.

end_beats property

end_beats: float

Where the note ends, in beats.

with_pitch

with_pitch(pitch: Pitch) -> TerminalNote

Return the note with another pitch.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_pitch(self, pitch: Pitch) -> TerminalNote:
    """Return the note with another pitch."""

with_start_beats

with_start_beats(start_beats: float) -> TerminalNote

Return the note starting at another beat.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_start_beats(self, start_beats: float) -> TerminalNote:
    """Return the note starting at another beat."""

with_duration_beats

with_duration_beats(duration_beats: float) -> TerminalNote

Return the note held for another number of beats.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_duration_beats(self, duration_beats: float) -> TerminalNote:
    """Return the note held for another number of beats."""

with_velocity

with_velocity(velocity: float) -> TerminalNote

Return the note with another strength.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_velocity(self, velocity: float) -> TerminalNote:
    """Return the note with another strength."""

transposed

transposed(semitones: int) -> TerminalNote

Return the note moved by semitones.

Raises:

  • ValueError –

    If the result leaves the MIDI range 0 to 127.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def transposed(self, semitones: int) -> TerminalNote:
    """
    Return the note moved by ``semitones``.

    Raises:
        ValueError: If the result leaves the MIDI range 0 to 127.
    """

__add__

__add__(other: int) -> TerminalNote

Move the note up by semitones, or play something after it.

note + 1 is the note one semitone higher, note + 12 an octave higher. Adding a note or a melody makes a TerminalMelody that plays them one after the other.

Raises:

  • ValueError –

    If the result leaves the MIDI range 0 to 127.

Examples:

assert TerminalNote("C4") + 1 == TerminalNote("C#4")
assert 12 + TerminalNote("C4") == TerminalNote("C5")
melody = TerminalNote("C4") + TerminalNote("E4")
assert [note.name for note in melody] == ["C4", "E4"]
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __add__(
    self, other: int | TerminalNote | TerminalMelody
) -> TerminalNote | TerminalMelody:
    """
    Move the note up by semitones, or play something after it.

    ``note + 1`` is the note one semitone higher, ``note + 12`` an octave higher.
    Adding a note or a melody makes a [`TerminalMelody`][xnano_audio.TerminalMelody]
    that plays them one after the other.

    Raises:
        ValueError: If the result leaves the MIDI range 0 to 127.

    Examples:
        ```python
        assert TerminalNote("C4") + 1 == TerminalNote("C#4")
        assert 12 + TerminalNote("C4") == TerminalNote("C5")
        melody = TerminalNote("C4") + TerminalNote("E4")
        assert [note.name for note in melody] == ["C4", "E4"]
        ```
    """

__sub__

__sub__(other: int) -> TerminalNote
__sub__(other: TerminalNote) -> int
__sub__(other: int | TerminalNote) -> TerminalNote | int

Move the note down by semitones, or measure the distance to another note.

Examples:

assert TerminalNote("C4") - 1 == TerminalNote("B3")
assert TerminalNote("E4") - TerminalNote("C4") == 4
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __sub__(self, other: int | TerminalNote) -> TerminalNote | int:
    """
    Move the note down by semitones, or measure the distance to another note.

    Examples:
        ```python
        assert TerminalNote("C4") - 1 == TerminalNote("B3")
        assert TerminalNote("E4") - TerminalNote("C4") == 4
        ```
    """

__or__

Play notes together, as a chord.

Examples:

chord = TerminalNote("C4") | TerminalNote("E4") | TerminalNote("G4")
assert {note.start_beats for note in chord} == {0.0}
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __or__(self, other: TerminalNote | TerminalMelody) -> TerminalMelody:
    """
    Play notes together, as a chord.

    Examples:
        ```python
        chord = TerminalNote("C4") | TerminalNote("E4") | TerminalNote("G4")
        assert {note.start_beats for note in chord} == {0.0}
        ```
    """

__int__

__int__() -> int

The MIDI pitch, so int(TerminalNote("C4")) is 60.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __int__(self) -> int:
    """The MIDI pitch, so ``int(TerminalNote("C4"))`` is ``60``."""

__lt__

__lt__(other: TerminalNote) -> bool

Notes are ordered by pitch, then by where they start.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __lt__(self, other: TerminalNote) -> bool:
    """Notes are ordered by pitch, then by where they start."""

TerminalSound

A rendered mono sound: raw samples plus their sample rate.

Immutable. Create one with render_note, then inspect it, run it through more effects, or play it with TerminalAudioPlayer.play_sound. len(sound) is the number of samples.

Attributes:

Methods:

  • from_samples –

    Build a sound from your own mono floats.

  • from_bytes –

    Build a sound from little-endian 32-bit float bytes.

  • to_bytes –

    The samples as little-endian 32-bit floats.

  • samples –

    The samples as a list of floats.

  • envelope –

    The loudness outline of the sound: points peak levels from 0.0 to 1.0.

  • with_effects –

    A new sound with effects applied in order.

  • mix –

    Mix sounds placed on a timeline into one sound.

  • add –

    Apply one or more audio effects (or effect chains) in order and return the new sound.

  • __or__ –

    Apply an audio effect or effect chain to this sound and return the new sound.

sample_rate property

sample_rate: int

Samples per second.

duration_milliseconds property

duration_milliseconds: float

Length in milliseconds, including any effect tail.

peak property

peak: float

Largest absolute sample value, from 0.0 to 1.0.

root_mean_square property

root_mean_square: float

Root-mean-square level, from 0.0 to 1.0.

from_samples staticmethod

from_samples(samples: list[float], *, sample_rate: int = 44100) -> TerminalSound

Build a sound from your own mono floats.

This is how to add a custom instrument or waveform: generate the samples in Python (from numpy, call .tolist()), then process and play them like any built-in sound.

Parameters:

  • samples (list[float]) –

    Values from -1.0 to 1.0; they are not rescaled.

  • sample_rate (int, default: 44100 ) –

    Samples per second, 8000 to 192000.

Raises:

  • ValueError –

    If samples is empty or contains NaN or infinity, or the rate is out of range.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def from_samples(samples: list[float], *, sample_rate: int = 44100) -> TerminalSound:
    """
    Build a sound from your own mono floats.

    This is how to add a custom instrument or waveform: generate the
    samples in Python (from numpy, call ``.tolist()``), then process and
    play them like any built-in sound.

    Args:
        samples: Values from -1.0 to 1.0; they are not rescaled.
        sample_rate: Samples per second, 8000 to 192000.

    Raises:
        ValueError: If ``samples`` is empty or contains NaN or infinity,
            or the rate is out of range.
    """

from_bytes staticmethod

from_bytes(data: bytes, *, sample_rate: int = 44100) -> TerminalSound

Build a sound from little-endian 32-bit float bytes.

The inverse of to_bytes.

Raises:

  • ValueError –

    If data is empty or its length is not a multiple of 4, it contains NaN or infinity, or the rate is out of range.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def from_bytes(data: bytes, *, sample_rate: int = 44100) -> TerminalSound:
    """
    Build a sound from little-endian 32-bit float bytes.

    The inverse of [`to_bytes`][xnano_audio.TerminalSound.to_bytes].

    Raises:
        ValueError: If ``data`` is empty or its length is not a multiple
            of 4, it contains NaN or infinity, or the rate is out of range.
    """

to_bytes

to_bytes() -> bytes

The samples as little-endian 32-bit floats.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def to_bytes(self) -> bytes:
    """The samples as little-endian 32-bit floats."""

samples

samples() -> list[float]

The samples as a list of floats.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def samples(self) -> list[float]:
    """The samples as a list of floats."""

envelope

envelope(points: int) -> list[float]

The loudness outline of the sound: points peak levels from 0.0 to 1.0.

Each value is the largest absolute sample in one equal slice of the sound, ready to draw as a waveform or a sparkline.

Raises:

  • ValueError –

    If points is not between 1 and 100000.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def envelope(self, points: int) -> list[float]:
    """
    The loudness outline of the sound: ``points`` peak levels from 0.0 to 1.0.

    Each value is the largest absolute sample in one equal slice of the
    sound, ready to draw as a waveform or a sparkline.

    Raises:
        ValueError: If ``points`` is not between 1 and 100000.
    """

with_effects

with_effects(effects: list[TerminalAudioEffect]) -> TerminalSound

A new sound with effects applied in order.

Echo and reverb tails make the result longer than the original. The peak is kept below 0.98 and the added tail is capped at 30 seconds. The GIL is released while processing.

Raises:

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def with_effects(self, effects: list[TerminalAudioEffect]) -> TerminalSound:
    """
    A new sound with ``effects`` applied in order.

    Echo and reverb tails make the result longer than the original. The
    peak is kept below 0.98 and the added tail is capped at 30 seconds.
    The GIL is released while processing.

    Raises:
        ValueError: If more than 32 effects are given.
        TypeError: If an item is not a
            [`TerminalAudioEffect`][xnano_audio.TerminalAudioEffect].
    """

mix staticmethod

mix(layers: Sequence[tuple[TerminalSound, float, float]], *, duration_seconds: float, start_seconds: float = 0.0, sample_rate: int = 44100) -> TerminalSound

Mix sounds placed on a timeline into one sound.

The result is the part of the timeline from start_seconds for duration_seconds. Each layer is added at its offset, multiplied by its gain, and the sum is limited to -1.0 to 1.0. A layer that starts before the window is cut at the window's start, and one that runs past its end is cut there. The GIL is released while mixing.

Parameters:

  • layers (Sequence[tuple[TerminalSound, float, float]]) –

    (sound, offset_seconds, gain) tuples. offset_seconds is where on the timeline the sound begins (it may be negative or before start_seconds) and gain is a factor of 0.0 or more.

  • duration_seconds (float) –

    The length of the mixed sound, above 0 and at most 3600.

  • start_seconds (float, default: 0.0 ) –

    The time on the timeline where the mixed sound begins.

  • sample_rate (int, default: 44100 ) –

    Samples per second, 8000 to 192000. Every layer must have been rendered at this rate.

Raises:

  • ValueError –

    If duration_seconds or sample_rate is out of range, the window holds more than 172800000 samples or cannot be allocated, a layer does not have exactly three values, an offset is not finite, a gain is not a finite number from 0.0 to 1000.0, or a layer was rendered at a different sample rate.

  • TypeError –

    If layers is not a sequence of (sound, offset_seconds, gain) tuples, or an offset or gain is not a number.

Examples:

beat = render_note("kick", "C2")
note = render_note("sine", "A4", duration_milliseconds=500)
window = TerminalSound.mix(
    [(beat, 0.0, 1.0), (note, 0.5, 0.5)],
    duration_seconds=1.0,
)
assert window.duration_milliseconds == 1000.0
assert window.peak <= 1.0
quiet = TerminalSound.mix([(note, 0.0, 0.0)], duration_seconds=0.25)
assert quiet.peak == 0.0
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
@staticmethod
def mix(
    layers: Sequence[tuple[TerminalSound, float, float]],
    *,
    duration_seconds: float,
    start_seconds: float = 0.0,
    sample_rate: int = 44100,
) -> TerminalSound:
    """
    Mix sounds placed on a timeline into one sound.

    The result is the part of the timeline from ``start_seconds`` for
    ``duration_seconds``. Each layer is added at its offset, multiplied by its gain, and
    the sum is limited to -1.0 to 1.0. A layer that starts before the window is cut at
    the window's start, and one that runs past its end is cut there. The GIL is released
    while mixing.

    Args:
        layers: ``(sound, offset_seconds, gain)`` tuples. ``offset_seconds`` is where on the
            timeline the sound begins (it may be negative or before ``start_seconds``) and
            ``gain`` is a factor of 0.0 or more.
        duration_seconds: The length of the mixed sound, above 0 and at most 3600.
        start_seconds: The time on the timeline where the mixed sound begins.
        sample_rate: Samples per second, 8000 to 192000. Every layer must have been
            rendered at this rate.

    Raises:
        ValueError: If ``duration_seconds`` or ``sample_rate`` is out of range, the window
            holds more than 172800000 samples or cannot be allocated, a layer does not have
            exactly three values, an offset is not finite, a gain is not a finite number from
            0.0 to 1000.0, or a layer was rendered at a different sample rate.
        TypeError: If ``layers`` is not a sequence of ``(sound, offset_seconds, gain)``
            tuples, or an offset or gain is not a number.

    Examples:
        ```python
        beat = render_note("kick", "C2")
        note = render_note("sine", "A4", duration_milliseconds=500)
        window = TerminalSound.mix(
            [(beat, 0.0, 1.0), (note, 0.5, 0.5)],
            duration_seconds=1.0,
        )
        assert window.duration_milliseconds == 1000.0
        assert window.peak <= 1.0
        quiet = TerminalSound.mix([(note, 0.0, 0.0)], duration_seconds=0.25)
        assert quiet.peak == 0.0
        ```
    """

add

add(*effects: AudioEffectSource) -> TerminalSound

Apply one or more audio effects (or effect chains) in order and return the new sound.

Examples:

sound = render_note("piano", "C4", duration_milliseconds=100).add(
    TerminalAudioEffect.low_pass(cutoff=900.0)
)
assert len(sound) > 0
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def add(self, *effects: AudioEffectSource) -> TerminalSound:
    """
    Apply one or more audio effects (or effect chains) in order and return the new sound.

    Examples:
        ```python
        sound = render_note("piano", "C4", duration_milliseconds=100).add(
            TerminalAudioEffect.low_pass(cutoff=900.0)
        )
        assert len(sound) > 0
        ```
    """

__or__

__or__(other: AudioEffectSource) -> TerminalSound

Apply an audio effect or effect chain to this sound and return the new sound.

Examples:

sound = (
    render_note("piano", "C4", duration_milliseconds=100)
    | TerminalAudioEffect.low_pass(cutoff=900.0)
    | TerminalAudioEffect.reverb(room=0.4)
)
assert len(sound) > 0
Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def __or__(self, other: AudioEffectSource) -> TerminalSound:
    """
    Apply an audio effect or effect chain to this sound and return the new sound.

    Examples:
        ```python
        sound = (
            render_note("piano", "C4", duration_milliseconds=100)
            | TerminalAudioEffect.low_pass(cutoff=900.0)
            | TerminalAudioEffect.reverb(room=0.4)
        )
        assert len(sound) > 0
        ```
    """

AudioDevice

AudioDevice(canvas: Canvas[object, object])

The canvas's sound: notes, melodies and sounds to play, a musical clock, and the microphone.

Get it from Canvas.audio. Playing never waits. Pitches are MIDI note numbers or names everywhere, and the length of a beat follows the canvas's bpm.

Where the sound goes depends on the canvas. A canvas that runs by itself (on the screen, or created with realtime=True) plays on this computer's sound output: the sound is rendered on your thread and handed to the canvas's own loop, which owns the device. A canvas from new() has no device and a clock you move, so it records what is played instead, and render_sound turns the recording into a sound, step by step, the same on every run.

The microphone level and the musical beat are expressions, so they drive anything an expression can: a position, a size, a color, an effect.

Attributes:

  • is_available (bool) –

    Whether sound can be played: always on a canvas without a device, and when this computer has a sound output device otherwise.

  • is_offline (bool) –

    Whether this canvas records sound instead of playing it on a device.

  • beat (NumericExpression) –

    The musical clock's position in beats, as an expression.

  • level (NumericExpression) –

    The microphone's current level from 0.0 to 1.0, as an expression.

Examples:

import xnano

with xnano.new(width=20, height=3) as canvas:
    meter = canvas.write('#', y=1)
    meter.x = canvas.audio.level * 19
    level = canvas.inspect(canvas.audio.level)
    assert 0.0 <= level <= 1.0
    assert canvas.audio.is_offline

Create the canvas's audio. Use canvas.audio instead of calling this.

Parameters:

Methods:

  • play –

    Play something that makes sound, and return at once.

  • render_sound –

    Turn what was played since the previous call into one sound, and forget it.

  • start –

    Start the musical clock.

  • resume –

    Let the musical clock run on from where pause left it.

  • pause –

    Pause the musical clock, keeping its position.

  • stop –

    Stop the musical clock and return it to the first beat.

  • level_history –

    Return the last count microphone levels, oldest first.

Source code in xnano/audio.py
def __init__(self, canvas: Canvas[object, object]) -> None:
    """
    Create the canvas's audio. Use ``canvas.audio`` instead of calling this.

    Args:
        canvas: The canvas the audio belongs to.
    """
    self._canvas = canvas
    self._playable_effects = PlayableEffects(self)
    self._lock = threading.Lock()
    self._player: TerminalAudioPlayer | None = None
    self._microphone: TerminalMicrophone | None = None
    self._unavailable: set[str] = set()
    self._is_held = False
    self._level = _external(_LEVEL_NAME, True)
    self._beat = _external(_BEAT_NAME, True)
    self._recordings: list[_Recording] = []
    self._pruned_at = canvas.time.elapsed
    self._rendered_until = canvas.time.elapsed
    self._synthesized: collections.OrderedDict[
        tuple[object, ...], tuple[object, TerminalSound]
    ] = collections.OrderedDict()
    # The musical clock of a canvas without a device: where it started, at what tempo,
    # and the beat it stood at while it was paused.
    self._clock_origin_seconds: float | None = None
    self._clock_bpm = canvas.bpm
    self._paused_beat = 0.0

is_offline property

is_offline: bool

Whether this canvas records sound instead of playing it on a device.

It is the opposite of Time.is_realtime: a canvas that runs on the wall clock plays on a device, and one whose clock you move records.

is_available property

is_available: bool

Whether sound can be played: always without a device, otherwise when one exists.

fx property

The sound effects of everything this canvas plays, one method for each effect.

canvas.audio.fx.reverb(mix=0.4) adds a reverb to every sound played after it, from any hook or from your own code, and canvas.audio.fx.clear() removes what was added here. A sound that is already playing keeps the effects it started with. See PlayableEffects.

Examples:

import xnano

with xnano.new(width=8, height=1) as canvas:
    canvas.audio.fx.reverb(room=0.8, mix=0.5)
    canvas.play('C4')
    canvas.step(delta_seconds=0.5)
    assert canvas.audio.render_sound().peak > 0.0

beat property

beat: NumericExpression

The musical clock's position in beats, as an expression.

It stays at 0 until the clock is started with start or a sound is played with at_beat.

Examples:

import xnano

with xnano.new(width=20, height=2) as canvas:
    ball = canvas.write('o')
    ball.x = (canvas.audio.beat % 1) * 19
    canvas.audio.start()
    canvas.step(delta_seconds=0.25)
    assert canvas.inspect(canvas.audio.beat) == 0.5

level property

level: NumericExpression

The microphone's current level from 0.0 to 1.0, as an expression.

Reading it opens the default input device the first time a frame evaluates it. Without an input device, without permission, or on a canvas without a device, the level is 0.

play

play(source: PlaySource, *, instrument: InstrumentName | None = None, beats: float | None = None, velocity: float | None = None, effects: Sequence[PlayableEffectSpecification] | None = None, volume: float = 1.0, pan: float = 0.0, at_beat: int | None = None) -> PlayHandle

Play something that makes sound, and return at once.

source is a Note, an Audio, the name of a registered symbol, a note name such as 'C#4', a MIDI number such as 60, a frequency in hertz, melody text, or a sequence of those that plays one after another. A str is a symbol name first; if no symbol has that name it is a note name, a frequency such as '440hz' or melody text. Melody text has spaces, :2 holds a note for two beats, rest is a silence, + joins a chord and | marks a bar: 'C4 E4 G4:2 rest C5+E5+G5'. An option you pass here replaces the same value on a Note, so play(note('C4'), instrument='violin') plays the violin.

On a canvas without a device the sound is recorded at the canvas's current time and nothing is synthesized until render_sound asks for it; the arguments are still checked here.

Parameters:

  • source (PlaySource) –

    What to play.

  • instrument (InstrumentName | None, default: None ) –

    A built-in instrument for notes and melodies, from instrument_names; 'piano' unless a Note says otherwise. A sound is played as it is.

  • beats (float | None, default: None ) –

    How long a note name, number, frequency or Note is held, in beats at the canvas's tempo, and how long melody text holds a note that gives no length of its own. 1.0 unless a Note says otherwise.

  • velocity (float | None, default: None ) –

    How hard a note is played, 0.0 to 1.0; 0.8 unless a Note says otherwise.

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

    Effects applied after the instrument, or to a sound as it is played, as dictionaries such as {'kind': 'audio.reverb'}. A hook that called context.fx with a sound effect adds its effects to every sound it plays.

  • volume (float, default: 1.0 ) –

    Playback gain, 0.0 to 1.0.

  • pan (float, default: 0.0 ) –

    Stereo position from -1.0 (left) to 1.0 (right). A recording is mono.

  • at_beat (int | None, default: None ) –

    Start when the musical clock reaches this beat. The clock is started if it is not running.

Returns:

Raises:

  • TerminalAudioUnavailableError –

    The canvas plays on a device, and this computer has no sound output device.

  • ValueError –

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

  • TypeError –

    source cannot be played, or an effect is not an audio effect.

Examples:

import xnano

with xnano.new(width=8, height=1) as canvas:
    canvas.audio.play('C4')
    canvas.audio.play(64, instrument='bell')
    canvas.audio.play(xnano.note('G4', beats=0.5))
    canvas.audio.play('C4 E4 G4:2 rest C5+E5+G5')
    canvas.step(delta_seconds=0.5)
    assert canvas.audio.render_sound().duration_milliseconds == 500.0
Source code in xnano/audio.py
def play(
    self,
    source: PlaySource,
    *,
    instrument: InstrumentName | None = None,
    beats: float | None = None,
    velocity: float | None = None,
    effects: Sequence[PlayableEffectSpecification] | None = None,
    volume: float = 1.0,
    pan: float = 0.0,
    at_beat: int | None = None,
) -> PlayHandle:
    """
    Play something that makes sound, and return at once.

    ``source`` is a [`Note`][xnano.Note], an [`Audio`][xnano.Audio], the name of a
    registered symbol, a note name such as ``'C#4'``, a MIDI number such as ``60``, a
    frequency in hertz, melody text, or a sequence of those that plays one after another.
    A ``str`` is a symbol name first; if no symbol has that name it is a note name, a
    frequency such as ``'440hz'`` or melody text. Melody text has spaces, ``:2`` holds a
    note for two beats, ``rest`` is a silence, ``+`` joins a chord and ``|`` marks a bar:
    ``'C4 E4 G4:2 rest C5+E5+G5'``. An option you pass here replaces the same value on a
    ``Note``, so ``play(note('C4'), instrument='violin')`` plays the violin.

    On a canvas without a device the sound is recorded at the canvas's current time and
    nothing is synthesized until [`render_sound`][xnano.audio.AudioDevice.render_sound]
    asks for it; the arguments are still checked here.

    Args:
        source: What to play.
        instrument: A built-in instrument for notes and melodies, from
            [`instrument_names`][xnano_audio.instrument_names]; ``'piano'`` unless a
            ``Note`` says otherwise. A sound is played as it is.
        beats: How long a note name, number, frequency or ``Note`` is held, in beats at
            the canvas's tempo, and how long melody text holds a note that gives no length
            of its own. ``1.0`` unless a ``Note`` says otherwise.
        velocity: How hard a note is played, 0.0 to 1.0; ``0.8`` unless a ``Note`` says
            otherwise.
        effects: Effects applied after the instrument, or to a sound as it is played,
            as dictionaries such as ``{'kind': 'audio.reverb'}``. A hook that called
            ``context.fx`` with a sound effect adds its effects to every sound it
            plays.
        volume: Playback gain, 0.0 to 1.0.
        pan: Stereo position from -1.0 (left) to 1.0 (right). A recording is mono.
        at_beat: Start when the musical clock reaches this beat. The clock is started if
            it is not running.

    Returns:
        A [`PlayHandle`][xnano.music.PlayHandle] that tells when the sound is over.

    Raises:
        TerminalAudioUnavailableError: The canvas plays on a device, and this computer has
            no sound output device.
        ValueError: An argument is out of range, the instrument is unknown, or a ``str``
            is neither a symbol, a note name nor melody text. The message lists the known
            sound symbols.
        TypeError: ``source`` cannot be played, or an effect is not an audio effect.

    Examples:
        ```python
        import xnano

        with xnano.new(width=8, height=1) as canvas:
            canvas.audio.play('C4')
            canvas.audio.play(64, instrument='bell')
            canvas.audio.play(xnano.note('G4', beats=0.5))
            canvas.audio.play('C4 E4 G4:2 rest C5+E5+G5')
            canvas.step(delta_seconds=0.5)
            assert canvas.audio.render_sound().duration_milliseconds == 500.0
        ```
    """
    canvas = self._canvas
    canvas._require_open()
    planned = _plan_call(
        source,
        bpm=canvas.bpm,
        instrument=instrument,
        beats=beats,
        velocity=velocity,
        effects=effects,
        volume=volume,
        pan=pan,
        at_beat=at_beat,
        extra_effects=_hook_effects(canvas) + self._playable_effects._audio_effects(),
    )
    if self.is_offline:
        return self._record(planned, volume=volume, at_beat=at_beat)
    if not TerminalAudioPlayer.is_available():
        raise TerminalAudioUnavailableError(
            'AudioDevice.play needs a sound output device, but this computer has none that '
            'can be opened. Check canvas.audio.is_available before playing, or play on a '
            'canvas from xnano.new(), which records the sound for render_sound().'
        )
    sound = _mix_planned(planned)
    handle = PlayHandle(
        clock=time.monotonic,
        end_seconds=time.monotonic() + sound.duration_milliseconds / 1000.0,
    )
    canvas._host.post(
        lambda: handle._is_removed or self._play_on_loop(sound, volume, pan, at_beat)
    )
    return handle

render_sound

render_sound() -> TerminalSound

Turn what was played since the previous call into one sound, and forget it.

The sound covers the canvas time from the previous render_sound() (or from when the audio was first used) up to now, so calling it after every step gives the sound of each step. Everything played in that time is synthesized and mixed (limited to -1.0 to 1.0, mono); a note that rings on past the end continues in the next call. A second call at the same time returns an empty instant of silence, because the first one took the sound. Sounds that ended more than 60 seconds of canvas time ago are not kept, so a canvas that has not been asked for longer than that returns the last 60 seconds.

Returns:

Raises:

  • TerminalError –

    The canvas plays on a device, which does not record.

Examples:

import xnano

with xnano.new(width=8, height=1, fps=10) as canvas:
    canvas.audio.play('C4', instrument='bell')
    canvas.step()
    sound = canvas.audio.render_sound()
    assert sound.duration_milliseconds == 100.0
    assert max(abs(sample) for sample in sound.samples()) > 0.0
Source code in xnano/audio.py
def render_sound(self) -> TerminalSound:
    """
    Turn what was played since the previous call into one sound, and forget it.

    The sound covers the canvas time from the previous ``render_sound()`` (or from when
    the audio was first used) up to now, so calling it after every
    [`step`][xnano.canvas.Canvas.step] gives the sound of each step. Everything played in
    that time is synthesized and mixed (limited to -1.0 to 1.0, mono); a note that rings
    on past the end continues in the next call. A second call at the same time returns
    an empty instant of silence, because the first one took the sound. Sounds that
    ended more than 60 seconds of canvas time ago are not kept, so a canvas that has not
    been asked for longer than that returns the last 60 seconds.

    Returns:
        The [`TerminalSound`][xnano_audio.TerminalSound] of that time.

    Raises:
        TerminalError: The canvas plays on a device, which does not record.

    Examples:
        ```python
        import xnano

        with xnano.new(width=8, height=1, fps=10) as canvas:
            canvas.audio.play('C4', instrument='bell')
            canvas.step()
            sound = canvas.audio.render_sound()
            assert sound.duration_milliseconds == 100.0
            assert max(abs(sample) for sample in sound.samples()) > 0.0
        ```
    """
    from xnano.errors import TerminalError

    canvas = self._canvas
    canvas._require_open()
    if not self.is_offline:
        raise TerminalError(
            'This canvas plays sound on a device, so there is no recording to render. '
            'render_sound() reads the sound of a canvas from xnano.new(), whose clock you '
            'move.'
        )
    now = canvas.time.elapsed
    with self._lock:
        start = max(self._rendered_until, now - _RETENTION_SECONDS)
        self._rendered_until = now
        playing = [
            item
            for item in self._recordings
            if item.start_seconds < now and item.end_seconds > start
        ]
        self._recordings = [
            item for item in self._recordings if item.end_seconds > now
        ]
    duration = now - start
    if duration <= 0.0:
        return TerminalSound.from_samples([0.0])
    layers = [
        (self._synthesized_sound(item), item.start_seconds, item.volume)
        for item in playing
    ]
    return TerminalSound.mix(layers, duration_seconds=duration, start_seconds=start)

start

start() -> None

Start the musical clock.

Source code in xnano/audio.py
def start(self) -> None:
    """Start the musical clock."""
    canvas = self._canvas
    canvas._require_open()
    if self.is_offline:
        if self._clock_origin_seconds is None:
            self._start_offline_clock(canvas.time.elapsed)
            if canvas._time_source.is_stopped:
                self._hold()
        return
    canvas._host.post(self._start_on_loop)

resume

resume() -> None

Let the musical clock run on from where pause left it.

Source code in xnano/audio.py
def resume(self) -> None:
    """Let the musical clock run on from where [`pause`][xnano.audio.AudioDevice.pause] left it."""
    self.start()

pause

pause() -> None

Pause the musical clock, keeping its position.

Source code in xnano/audio.py
def pause(self) -> None:
    """Pause the musical clock, keeping its position."""
    canvas = self._canvas
    canvas._require_open()
    if self.is_offline:
        self._pause_offline_clock()
        return
    canvas._host.post(lambda: self._player and self._player.pause_clock())

stop

stop() -> None

Stop the musical clock and return it to the first beat.

This rewinds only the musical clock. To freeze the whole canvas, including this clock, use Canvas.stop; it resumes from where it stood with Canvas.resume.

Source code in xnano/audio.py
def stop(self) -> None:
    """
    Stop the musical clock and return it to the first beat.

    This rewinds only the musical clock. To freeze the whole canvas, including this
    clock, use [`Canvas.stop`][xnano.canvas.Canvas.stop]; it resumes from where it stood
    with [`Canvas.resume`][xnano.canvas.Canvas.resume].
    """
    canvas = self._canvas
    canvas._require_open()
    if self.is_offline:
        self._clock_origin_seconds = None
        self._paused_beat = 0.0
        return
    canvas._host.post(lambda: self._player and self._player.stop_clock())

level_history

level_history(count: int) -> list[float]

Return the last count microphone levels, oldest first.

Parameters:

  • count (int) –

    How many levels to return.

Source code in xnano/audio.py
def level_history(self, count: int) -> list[float]:
    """
    Return the last ``count`` microphone levels, oldest first.

    Args:
        count: How many levels to return.
    """
    canvas = self._canvas
    canvas._require_open()

    def read() -> list[float]:
        microphone = self._open_microphone()
        return [] if microphone is None else microphone.level_history(count)

    return canvas._host.call(read)

instrument_names

instrument_names() -> list[InstrumentName]

Names of every built-in instrument, in catalog order.

Source code in packages/xnano-audio/src/xnano_audio/_xnano_audio/__init__.pyi
def instrument_names() -> list[InstrumentName]:
    """Names of every built-in instrument, in catalog order."""