Skip to content

Midi

midi

MIDI numbers, frequencies, and chromatic operations.

The Midi namespace handles conversions between MIDI note numbers (0-127), note names, and frequencies in Hz. It also exposes lower-level pitch-class-set helpers used by Pcset and Scale internally.

The reference frequency is A4 = 440 Hz (MIDI 69). All conversions follow the standard equal-temperament tuning unless an alternate tuning argument is provided.

Example

from tonal_py import Midi Midi.to_midi("C4") 60 Midi.midi_to_freq(69) 440.0 Midi.midi_to_note_name(60) 'C4'

Source parity: @tonaljs/midi

is_midi

is_midi(arg: Any) -> bool

True if arg (number or numeric string) is in the MIDI range 0-127.

Boolean inputs are rejected. Strings that parse as a number are accepted (mirrors JS +arg).

Parameters:

Name Type Description Default
arg Any

Any value.

required

Returns:

Type Description
bool

True iff arg coerces to a number in [0, 127].

Example

from tonal_py import Midi Midi.is_midi(60) True Midi.is_midi("60") True Midi.is_midi(128) False Midi.is_midi(-1) False

Source code in src/tonal_py/midi.py
def is_midi(arg: Any) -> bool:
    """True if `arg` (number or numeric string) is in the MIDI range 0-127.

    Boolean inputs are rejected. Strings that parse as a number are
    accepted (mirrors JS `+arg`).

    Args:
        arg: Any value.

    Returns:
        True iff `arg` coerces to a number in [0, 127].

    Example:
        >>> from tonal_py import Midi
        >>> Midi.is_midi(60)
        True
        >>> Midi.is_midi("60")
        True
        >>> Midi.is_midi(128)
        False
        >>> Midi.is_midi(-1)
        False
    """
    if isinstance(arg, bool):
        return False
    try:
        n = float(arg)
    except (TypeError, ValueError):
        return False
    return 0 <= n <= 127

to_midi

to_midi(note: str | int | float) -> int | None

Get the MIDI number of a note name or pass through a valid MIDI value.

Parameters:

Name Type Description Default
note str | int | float

A note name ("C4"), a MIDI integer (0-127), or a numeric string ("60").

required

Returns:

Type Description
int | None

The MIDI number, or None if input is invalid or the note has

int | None

no MIDI value (pitch class without an octave).

Example

from tonal_py import Midi Midi.to_midi("C4") 60 Midi.to_midi(60) 60 Midi.to_midi("60") 60 Midi.to_midi("garbage") is None True

Source code in src/tonal_py/midi.py
def to_midi(note: str | int | float) -> int | None:
    """Get the MIDI number of a note name or pass through a valid MIDI value.

    Args:
        note: A note name (`"C4"`), a MIDI integer (0-127), or a numeric
            string (`"60"`).

    Returns:
        The MIDI number, or `None` if input is invalid or the note has
        no MIDI value (pitch class without an octave).

    Example:
        >>> from tonal_py import Midi
        >>> Midi.to_midi("C4")
        60
        >>> Midi.to_midi(60)
        60
        >>> Midi.to_midi("60")
        60
        >>> Midi.to_midi("garbage") is None
        True
    """
    if is_midi(note):
        # JS returns +arg (a Number). For integer-like inputs we return int.
        n = float(note)
        return int(n) if n.is_integer() else None  # type: ignore[return-value]
    n_obj = _pitch_note.note(note)
    return None if n_obj.empty else n_obj.midi

midi_to_freq

midi_to_freq(midi: float, tuning: float = 440) -> float

Convert MIDI to frequency in Hz.

Parameters:

Name Type Description Default
midi float

MIDI number (typically 0-127, but any value accepted).

required
tuning float

Reference frequency for A4. Default 440 Hz.

440

Returns:

Type Description
float

Frequency in hertz.

Example

from tonal_py import Midi Midi.midi_to_freq(69) 440.0 Midi.midi_to_freq(69, tuning=432) # alternate tuning 432.0 round(Midi.midi_to_freq(60), 4) 261.6256

Source code in src/tonal_py/midi.py
def midi_to_freq(midi: float, tuning: float = 440) -> float:
    """Convert MIDI to frequency in Hz.

    Args:
        midi: MIDI number (typically 0-127, but any value accepted).
        tuning: Reference frequency for A4. Default 440 Hz.

    Returns:
        Frequency in hertz.

    Example:
        >>> from tonal_py import Midi
        >>> Midi.midi_to_freq(69)
        440.0
        >>> Midi.midi_to_freq(69, tuning=432)       # alternate tuning
        432.0
        >>> round(Midi.midi_to_freq(60), 4)
        261.6256
    """
    return (2 ** ((midi - 69) / 12)) * tuning

freq_to_midi

freq_to_midi(freq: float) -> float

Convert frequency in Hz to MIDI (with two-decimal precision).

Returns a float rather than int because non-A=440 frequencies typically don't fall exactly on a MIDI boundary. Rounded to two decimals using JS-style half-up rounding (not Python's banker's rounding) for output parity with tonal.js.

Parameters:

Name Type Description Default
freq float

Frequency in hertz.

required

Returns:

Type Description
float

MIDI number with up to two decimal digits of precision.

Example

from tonal_py import Midi Midi.freq_to_midi(440) 69.0 Midi.freq_to_midi(220) 57.0 Midi.freq_to_midi(261.62) # close to but not exactly C4 60.0

Source code in src/tonal_py/midi.py
def freq_to_midi(freq: float) -> float:
    """Convert frequency in Hz to MIDI (with two-decimal precision).

    Returns a `float` rather than `int` because non-A=440 frequencies
    typically don't fall exactly on a MIDI boundary. Rounded to two
    decimals using JS-style half-up rounding (not Python's banker's
    rounding) for output parity with tonal.js.

    Args:
        freq: Frequency in hertz.

    Returns:
        MIDI number with up to two decimal digits of precision.

    Example:
        >>> from tonal_py import Midi
        >>> Midi.freq_to_midi(440)
        69.0
        >>> Midi.freq_to_midi(220)
        57.0
        >>> Midi.freq_to_midi(261.62)       # close to but not exactly C4
        60.0
    """
    v = 12 * (log(freq) - _L440) / _L2 + 69
    return _round_half_up(v * 100) / 100

midi_to_note_name

midi_to_note_name(midi: float, options: ToNoteNameOptions | None = None) -> str

Convert MIDI number to note name. Uses flats by default.

Parameters:

Name Type Description Default
midi float

MIDI number. Decimal values are rounded to nearest int.

required
options ToNoteNameOptions | None

Optional dict with "sharps": True to use sharps and/or "pitchClass": True to omit the octave number.

None

Returns:

Type Description
str

The note name (e.g. "C4", "Db5"). Returns "" for NaN or

str

infinite input.

Example

from tonal_py import Midi Midi.midi_to_note_name(60) 'C4' Midi.midi_to_note_name(61) # default: flats 'Db4' Midi.midi_to_note_name(61, {"sharps": True}) 'C#4' Midi.midi_to_note_name(61, {"pitchClass": True}) # no octave 'Db' Midi.midi_to_note_name(61.7) # rounds 'D4'

Source code in src/tonal_py/midi.py
def midi_to_note_name(midi: float, options: ToNoteNameOptions | None = None) -> str:
    """Convert MIDI number to note name. Uses flats by default.

    Args:
        midi: MIDI number. Decimal values are rounded to nearest int.
        options: Optional dict with `"sharps": True` to use sharps and/or
            `"pitchClass": True` to omit the octave number.

    Returns:
        The note name (e.g. `"C4"`, `"Db5"`). Returns `""` for NaN or
        infinite input.

    Example:
        >>> from tonal_py import Midi
        >>> Midi.midi_to_note_name(60)
        'C4'
        >>> Midi.midi_to_note_name(61)                        # default: flats
        'Db4'
        >>> Midi.midi_to_note_name(61, {"sharps": True})
        'C#4'
        >>> Midi.midi_to_note_name(61, {"pitchClass": True})  # no octave
        'Db'
        >>> Midi.midi_to_note_name(61.7)                      # rounds
        'D4'
    """
    if midi != midi or midi == float("inf") or midi == float("-inf"):
        return ""
    options = options or {}
    midi_int = _round_half_up(float(midi))
    pcs = _SHARPS if options.get("sharps") is True else _FLATS
    pc = pcs[midi_int % 12]
    if options.get("pitchClass"):
        return pc
    octave = midi_int // 12 - 1
    return pc + str(octave)

chroma

chroma(midi: int) -> int

Get the pitch class number 0-11 from a MIDI value.

Example

from tonal_py import Midi Midi.chroma(60) # C 0 Midi.chroma(61) # C#/Db 1 Midi.chroma(72) # C an octave up 0

Source code in src/tonal_py/midi.py
def chroma(midi: int) -> int:
    """Get the pitch class number 0-11 from a MIDI value.

    Example:
        >>> from tonal_py import Midi
        >>> Midi.chroma(60)        # C
        0
        >>> Midi.chroma(61)        # C#/Db
        1
        >>> Midi.chroma(72)        # C an octave up
        0
    """
    return midi % 12

pcset

pcset(notes: list[int] | str) -> list[int]

Get the unique pitch class set from MIDI numbers or a chroma string.

Parameters:

Name Type Description Default
notes list[int] | str

Either a list of MIDI numbers (will be deduplicated and sorted) or a 12-bit chroma string (positions of 1s returned as ints).

required

Returns:

Type Description
list[int]

Sorted list of unique chromas 0-11.

Example

from tonal_py import Midi Midi.pcset([60, 64, 67, 60, 71]) # C, E, G, C, B (dedup C) [0, 4, 7, 11] Midi.pcset("100100100101") [0, 3, 6, 9, 11]

Source code in src/tonal_py/midi.py
def pcset(notes: list[int] | str) -> list[int]:
    """Get the unique pitch class set from MIDI numbers or a chroma string.

    Args:
        notes: Either a list of MIDI numbers (will be deduplicated and
            sorted) or a 12-bit chroma string (positions of `1`s
            returned as ints).

    Returns:
        Sorted list of unique chromas 0-11.

    Example:
        >>> from tonal_py import Midi
        >>> Midi.pcset([60, 64, 67, 60, 71])      # C, E, G, C, B (dedup C)
        [0, 4, 7, 11]
        >>> Midi.pcset("100100100101")
        [0, 3, 6, 9, 11]
    """
    if isinstance(notes, list):
        return _pcset_from_midi(notes)
    return _pcset_from_chroma(notes)

pcset_nearest

pcset_nearest(notes: list[int] | str) -> Callable[[int], int | None]

Build a function that snaps a MIDI value to the nearest in-set pitch class.

On ties, prefers the higher MIDI (mirrors JS scan order).

Parameters:

Name Type Description Default
notes list[int] | str

Pitch class set (list of MIDI or chroma string).

required

Returns:

Type Description
Callable[[int], int | None]

A function (midi: int) -> int | None.

Example

from tonal_py import Midi snap = Midi.pcset_nearest([0, 4, 7]) # C major triad chromas snap(60) # already a C 60 snap(62) # D — equidistant from C and E; tie → E 64

Source code in src/tonal_py/midi.py
def pcset_nearest(notes: list[int] | str) -> Callable[[int], int | None]:
    """Build a function that snaps a MIDI value to the nearest in-set pitch class.

    On ties, prefers the higher MIDI (mirrors JS scan order).

    Args:
        notes: Pitch class set (list of MIDI or chroma string).

    Returns:
        A function `(midi: int) -> int | None`.

    Example:
        >>> from tonal_py import Midi
        >>> snap = Midi.pcset_nearest([0, 4, 7])    # C major triad chromas
        >>> snap(60)        # already a C
        60
        >>> snap(62)        # D — equidistant from C and E; tie → E
        64
    """
    s = pcset(notes)

    def fn(midi: int) -> int | None:
        ch = chroma(midi)
        for i in range(12):
            if (ch + i) in s:
                return midi + i
            if (ch - i) in s:
                return midi - i
        return None

    return fn

pcset_steps

pcset_steps(notes: list[int] | str, tonic: int) -> Callable[[int], int]

Build a 0-based step-to-MIDI mapper for a pitch class set + tonic.

Step 0 is the tonic; positive steps walk up the set (wrapping octaves as needed); negative steps walk down.

Parameters:

Name Type Description Default
notes list[int] | str

The pitch class set.

required
tonic int

The starting MIDI value.

required

Returns:

Type Description
Callable[[int], int]

A function (step: int) -> int.

Example

from tonal_py import Midi step = Midi.pcset_steps([0, 4, 7], 60) # C major triad, root C4 step(0) 60 step(2) 67 step(3) # wraps to next octave's tonic 72

Source code in src/tonal_py/midi.py
def pcset_steps(notes: list[int] | str, tonic: int) -> Callable[[int], int]:
    """Build a 0-based step-to-MIDI mapper for a pitch class set + tonic.

    Step 0 is the tonic; positive steps walk up the set (wrapping octaves
    as needed); negative steps walk down.

    Args:
        notes: The pitch class set.
        tonic: The starting MIDI value.

    Returns:
        A function `(step: int) -> int`.

    Example:
        >>> from tonal_py import Midi
        >>> step = Midi.pcset_steps([0, 4, 7], 60)    # C major triad, root C4
        >>> step(0)
        60
        >>> step(2)
        67
        >>> step(3)        # wraps to next octave's tonic
        72
    """
    s = pcset(notes)
    length = len(s)

    def fn(step: int) -> int:
        if step < 0:
            index = (length - (-step) % length) % length
        else:
            index = step % length
        octaves = floor(step / length)
        return s[index] + octaves * 12 + tonic

    return fn

pcset_degrees

pcset_degrees(notes: list[int] | str, tonic: int) -> Callable[[int], int | None]

Build a 1-based degree-to-MIDI mapper for a pitch class set + tonic.

Like pcset_steps but 1-indexed: degree 1 is the tonic, degree 0 is invalid (returns None).

Parameters:

Name Type Description Default
notes list[int] | str

The pitch class set.

required
tonic int

The starting MIDI value.

required

Returns:

Type Description
Callable[[int], int | None]

A function (degree: int) -> int | None.

Example

from tonal_py import Midi deg = Midi.pcset_degrees([0, 4, 7], 60) deg(1) 60 deg(0) is None # degree 0 invalid in 1-based system True

Source code in src/tonal_py/midi.py
def pcset_degrees(
    notes: list[int] | str, tonic: int
) -> Callable[[int], int | None]:
    """Build a 1-based degree-to-MIDI mapper for a pitch class set + tonic.

    Like [`pcset_steps`][tonal_py.midi.pcset_steps] but 1-indexed: degree
    1 is the tonic, degree 0 is invalid (returns None).

    Args:
        notes: The pitch class set.
        tonic: The starting MIDI value.

    Returns:
        A function `(degree: int) -> int | None`.

    Example:
        >>> from tonal_py import Midi
        >>> deg = Midi.pcset_degrees([0, 4, 7], 60)
        >>> deg(1)
        60
        >>> deg(0) is None         # degree 0 invalid in 1-based system
        True
    """
    steps = pcset_steps(notes, tonic)

    def fn(degree: int) -> int | None:
        if degree == 0:
            return None
        return steps(degree - 1 if degree > 0 else degree)

    return fn