Skip to main content

Real-time counterpoint engine proving music IS constraint satisfaction

Project description

counterpoint-engine

๐ŸŽต Species counterpoint as constraint satisfaction โ€” every rule returns SAT/UNSAT, voices form a Laman graph.

What It Does

Generates multi-voice counterpoint against a cantus firmus using backtracking search over musical constraints, then outputs the result as Tensor-MIDI events. Each contrapuntal rule is a predicate returning "SAT" or "UNSAT"; each voice is a vertex in a Laman graph; every constraint is an edge.

Why It Exists

Species counterpoint has been taught as a set of prohibitions for centuries. This library treats those prohibitions as constraint predicates and proves that the constraint graph on N voices is a Laman graph (2Nโˆ’3 edges, minimally rigid). That guarantees no voice is redundant and every rule is load-bearing. If you remove any edge, the structure gains a degree of freedom โ€” a voice can drift unconstrained.

The math: a set of N points in the plane is rigid iff the bar-and-joint framework on those points is Laman. Counterpoint voices are the points; interval constraints are the bars.

Quick Start

pip install -e .
from counterpoint_engine.generator import CounterpointGenerator, Species, Scale, VoiceRange

# Define a cantus firmus (C major, 8 notes)
cantus = [60, 62, 64, 65, 67, 69, 71, 72]  # C D E F G A B C

gen = CounterpointGenerator(
    cantus_firmus=cantus,
    species=Species.FIRST,
    scale=Scale(tonic=0, mode="major"),
    voice_range=VoiceRange(min_pitch=48, max_pitch=67),
)

counterpoint = gen.generate()
print(counterpoint)
# โ†’ [48, 53, 52, 50, 48, 48, 50, 48]

# Multi-voice โ€” Laman graph guarantees independence
voices = gen.generate_n_voices(n_voices=4)
# voices[0] = cantus firmus, voices[1..3] = generated

# Tensor-MIDI output
from counterpoint_engine.tensor_output import voices_to_tensor_events
tensor_events, midi_events = voices_to_tensor_events(voices)
print(tensor_events[0].to_bytes())  # b'\x3c\x00\x00\x0c'

API Overview

Rules (counterpoint_engine.rules)

Every rule returns the string "SAT" or "UNSAT".

from counterpoint_engine.rules import (
    no_parallel_fifths, no_parallel_octaves, proper_resolution,
    max_leap_seventh, consonant_interval, voice_independence, SAT, UNSAT
)

voice_a = [60, 62, 64, 65]
voice_b = [67, 69, 67, 69]
beats = [0, 1, 2, 3]

assert no_parallel_fifths(voice_a, voice_b, beats) == SAT
assert consonant_interval(voice_a, voice_b, 0) == SAT
Function Signature What it checks
no_parallel_fifths (voice_a, voice_b, beats) โ†’ str No consecutive perfect fifths in similar motion
no_parallel_octaves (voice_a, voice_b, beats) โ†’ str No consecutive perfect octaves in similar motion
proper_resolution (voice, beat, key_tonic, key_leading) โ†’ str Leading tone resolves to tonic
max_leap_seventh (voice, beat, max_leap) โ†’ str Melodic leap โ‰ค minor seventh (10 semitones)
consonant_interval (voice_a, voice_b, beat, allowed) โ†’ str Interval at beat is a consonance
voice_independence (laman_check: bool) โ†’ str Constraint graph is Laman rigid

Laman Graphs (counterpoint_engine.laman_counterpoint)

from counterpoint_engine.laman_counterpoint import (
    CounterpointGraph, henneberg_construct, verify_rigidity
)

graph = CounterpointGraph(n_voices=4)
print(graph.edges)              # [(0,1), (0,2), (1,2), ...]
print(graph.verify_rigidity())  # True
print(graph.edge_count())       # 5 (= 2*4 - 3)
print(graph.is_minimally_rigid())  # True

edges = henneberg_construct(4, seed=42)
assert verify_rigidity(4, edges)
Class/Function Description
CounterpointGraph Laman graph with add_constraint(), verify_rigidity(), is_minimally_rigid()
henneberg_construct(n, seed) Build a Laman graph via Henneberg type-I construction
verify_rigidity(n_voices, edges) Check Laman conditions (2Nโˆ’3 edges + subset condition)

Generator (counterpoint_engine.generator)

from counterpoint_engine.generator import CounterpointGenerator, Species, Scale, VoiceRange

gen = CounterpointGenerator(
    cantus_firmus=[60, 62, 64, 65, 67, 69, 71, 72],
    species=Species.FIRST,
    scale=Scale(tonic=0, mode="major"),
    voice_range=VoiceRange(min_pitch=48, max_pitch=72),
)

# Single voice
counterpoint = gen.generate()

# Multi-voice
voices = gen.generate_n_voices(n_voices=4)
Class/Enum Key attributes
Species FIRST, SECOND, THIRD, FOURTH, FIFTH (IntEnum 1โ€“5)
VoiceRange min_pitch, max_pitch, candidates(scale, prev_pitch)
Scale tonic, mode ("major"/"minor"), contains(pitch), pitch_classes()
CounterpointGenerator generate(), generate_n_voices(n, ranges)

Tensor-MIDI Output (counterpoint_engine.tensor_output)

from counterpoint_engine.tensor_output import (
    voices_to_tensor_events, voice_leading_to_sidechannels,
    interval_to_flux_vector, voice_intervals_to_flux_vectors,
    TensorMIDIEvent
)

tensor_events, midi_events = voices_to_tensor_events(voices)
raw = tensor_events[0].to_bytes()  # 4 bytes: cos, sin, beat, state

gestures = voice_leading_to_sidechannels(voices, beat=2)
# {(0,1): "Smile", (0,2): "Nod", (1,2): "Frown"}

fv = interval_to_flux_vector(7)  # perfect fifth โ†’ FluxVector
Function Returns
voices_to_tensor_events(voices) (List[TensorMIDIEvent], List[MidiEvent])
voice_leading_to_sidechannels(voices, beat) Dict[(i,j), str] โ€” Nod/Smile/Frown
interval_to_flux_vector(interval) FluxVector via Aโ‚‚ lattice
voice_intervals_to_flux_vectors(voices, beat) List[FluxVector]

Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   counterpoint-engine                โ”‚
โ”‚                                                     โ”‚
โ”‚  rules.py          laman_counterpoint.py             โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”             โ”‚
โ”‚  โ”‚ SAT/UNSATโ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”‚ CounterpointGraphโ”‚             โ”‚
โ”‚  โ”‚ kernels  โ”‚      โ”‚ henneberg_constructโ”‚            โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ”‚
โ”‚       โ”‚                     โ”‚                        โ”‚
โ”‚       โ–ผ                     โ–ผ                        โ”‚
โ”‚  generator.py                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”               โ”‚
โ”‚  โ”‚ CounterpointGenerator            โ”‚               โ”‚
โ”‚  โ”‚  .generate() โ†’ List[int]         โ”‚               โ”‚
โ”‚  โ”‚  .generate_n_voices() โ†’ voices   โ”‚               โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ”‚
โ”‚                 โ”‚                                    โ”‚
โ”‚                 โ–ผ                                    โ”‚
โ”‚  tensor_output.py                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”               โ”‚
โ”‚  โ”‚ voices_to_tensor_events()        โ”‚               โ”‚
โ”‚  โ”‚ voice_leading_to_sidechannels()  โ”‚               โ”‚
โ”‚  โ”‚ interval_to_flux_vector()        โ”‚               โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ”‚
โ”‚                                                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Dependencies                                       โ”‚
โ”‚  constraint-theory-core โ”€ Laman rigidity, Aโ‚‚ latticeโ”‚
โ”‚  flux-tensor-midi โ”€ FluxVector, MidiEvent types     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Data flow: cantus firmus โ†’ generator (backtracking) โ†’ voices โ†’ tensor_output โ†’ TensorMIDIEvent stream

Documentation

Ecosystem

Requirements

  • Python โ‰ฅ 3.10
  • constraint-theory-core (from ../constraint-theory-core, add to PYTHONPATH)
  • flux-tensor-midi (Tensor-MIDI event types)

Installation

pip install counterpoint-engine

Or install from source with dependencies:

pip install constraint-theory-core flux-tensor-midi
git clone https://github.com/SuperInstance/counterpoint-engine.git
cd counterpoint-engine
pip install -e ".[dev]"
pytest

Status

Tests Version License

All 78 tests pass.

License

Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

counterpoint_engine-0.3.0.tar.gz (52.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

counterpoint_engine-0.3.0-py3-none-any.whl (41.9 kB view details)

Uploaded Python 3

File details

Details for the file counterpoint_engine-0.3.0.tar.gz.

File metadata

  • Download URL: counterpoint_engine-0.3.0.tar.gz
  • Upload date:
  • Size: 52.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for counterpoint_engine-0.3.0.tar.gz
Algorithm Hash digest
SHA256 b57ca8db48590a467396d1f7e2cb9b9616f61d1769e7d58860fa85551d14b7df
MD5 791065b1a00ed6a9e7ce9a2c2ebb4d0d
BLAKE2b-256 effee395e17ac07dd4dfe1c34498a0a287f55e75fc2d36c429cf8fbfefc1dde3

See more details on using hashes here.

File details

Details for the file counterpoint_engine-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for counterpoint_engine-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 665f634faa8795724f345b193c7272e1d2a4317e02e40d5404e42624a27cad96
MD5 c8d4f5a1302cc8a30dc782d6efdb10be
BLAKE2b-256 0c0e8b332c1d6243a877e438bd6223ff3d9f710338b84415e517b571c094fe6f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page