The libcantus package is the C++ core called through ctypes. It exposes the same functions, classes and constants as the TypeScript package, with Python spelling, and returns plain dicts and lists. Behaviour and error messages match the TypeScript reference; the few differences are listed in C++ core and surface differences.
Python 3.10 or later.
pip install libcantusThe wheel bundles the shared library for its platform. In a source checkout the library is taken from build-shared/ (make build-shared); set the CANTUS_LIB environment variable to load another file. The library is loaded on the first call, so importing the package needs nothing installed.
import libcantus
libcantus.detect_chord_best([60, 64, 67])
# {'rootPc': 0, 'quality': 'maj', 'intervals': [0, 4, 7]}
chords = [[48, 60, 64, 67], [41, 60, 65, 69], [43, 59, 62, 65], [48, 60, 64, 67]]
notes = [
{"pitch": pitch, "startBeat": bar * 4, "durationBeat": 4}
for bar, pitches in enumerate(chords)
for pitch in pitches
]
timeline = libcantus.Score.of(notes).timeline()
[entry["roman"] for entry in timeline.roman()]
# ['I', 'IV', 'V7', 'I']Score, Chord, Key and the other model classes keep their names and their methods; static methods are snake_case (Score.from_data). Getters are properties (score.total_beats).
- Function and method names are the
snake_caseform of the TypeScript names:detectChordisdetect_chord,Score#withTempoiswith_tempo. - An optional options object (
opts/options) becomes keyword-only arguments, also insnake_case:detect_chord(pitches, input="midi", bass_pc=0). It does so wherever it sits in the parameter list, soRhythm.deform(rate=0.5)works even though actxparameter follows. A required options object, as ingenerate_progression,Composer(...)andresolve_meters, is the first positional argument and is a dict with TypeScript field names. The field names inside a plain dict that you pass keep their TypeScript spelling (startBeat,durationBeat), because that dict is the data itself. - Keyword arguments default to
None, andNonemeans not given. A positionalNone, or aNoneinside a dict or list, is sent as JSONnull, so it is validated exactly as TypeScript validatesnull. - An unknown keyword argument is a
TypeError, raised by Python. TypeScript ignores unknown option keys. intcrosses as a decimal integer,floatincludingnanandinfas a tagged number, andboolas a boolean.boolandstrwhere a number is expected are rejected with the TypeScript message.
Results are dicts and lists with the TypeScript field names. Three kinds of value come back as objects:
- Model instances (
Score,Timeline,Chord, …) are thin immutable wrappers. Their methods and properties call into the core, andreprshows their data. - Hybrids such as the
ChordTimelinereturned bychord_timeline_from_chordsexpose their data fields as attributes and their methods as methods:timeline.segments,timeline.at(5). - Handles are described below.
Constants are read from the module (libcantus.NAMED_SCALES, libcantus.ALGORITHM_VERSION). libcantus.version() and libcantus.algorithm_version() report the loaded library, and libcantus.list_ops() lists the registered ops.
State that is mutable or expensive to rebuild stays inside the core and is held by a handle: Rng, NoteEventIndex and ArrangementSession. Release one with close() or a with block; garbage collection releases a forgotten one as a fallback.
import libcantus
with libcantus.create_rng(1) as rng:
rng.next()
# 0.6270739405881613Using a closed handle raises CantusError with code RELEASED_HANDLE. Each handle serialises its own calls with a lock; do not share one across threads for concurrent use.
Where the TypeScript API takes a function, pass a Python callable. It receives the same arguments as in TypeScript, including the index.
import libcantus
notes = [{"pitch": pitch, "startBeat": beat, "durationBeat": 1} for beat, pitch in enumerate([60, 64, 67])]
libcantus.Score.of(notes).filter(lambda note, index: note["pitch"] > 60).notes
# [{'pitch': 64, 'startBeat': 1, 'durationBeat': 1}, {'pitch': 67, 'startBeat': 2, 'durationBeat': 1}]An exception raised inside a callback propagates out of the call as the original exception, not wrapped.
| Class | Also a | Raised for |
|---|---|---|
CantusError |
Exception |
Base of every error below. Has code. |
InvalidInputError |
ValueError |
INVALID_INPUT: a rejected argument. |
NoSolutionError |
NO_SOLUTION: valid input, no answer. Has at. |
|
BudgetExceededError |
ValueError |
BUDGET_EXCEEDED: the request is larger than the budget. |
code is a member of libcantus.ErrorCode, an IntEnum whose values are the status numbers in Errors and validation. Codes with no dedicated class (HOST_TYPE_ERROR, UNKNOWN_OP, BAD_REQUEST, RELEASED_HANDLE, INTERNAL) raise CantusError itself. libcantus.is_libcantus_error(value) is the type guard.
import libcantus
try:
libcantus.detect_chord([128])
except libcantus.InvalidInputError as error:
error.code == libcantus.ErrorCode.INVALID_INPUT
str(error)
# 'pitches[0] must be an integer in [0, 127]; received 128'python -m libcantus provides the same contract as the cantus tool, without a console script: python -m libcantus detectChordBest '[[60, 64, 67]]'. See Command line.
The package ships py.typed and a .pyi generated from the same manifest as the functions, so editors and mypy --strict see the keyword names. Return types are Any, because results are plain data.