Skip to main content

Standalone music theory primitives and harmonic relation analysis tools.

Project description

composer_theory 文档

本文档分为两篇。

第一篇讨论 composer_theory.domain 中的基础乐理对象。
第二篇讨论 composer_theory.relationscomposer_theory.resolve 中的关系命中对象与解析器。

在本文档中,重点不放在“调用技巧”,而放在“形式化定义、对象之间的结构关系,以及公开接口的语义边界”。


API 参考

完整的 API 参考文档见 API.md

该文档覆盖当前公开模块中的全部公开类、它们的公开属性、公开方法,以及本包核心的索引 / 运算符协议(如 []in|+-)。


目录


基础乐理篇

本篇对应下列代码文件:

  • domain/enums/__init__.py
  • domain/enums/_lookup.py
  • domain/enums/core.py
  • domain/enums/harmony.py
  • domain/enums/runtime.py
  • domain/enums/texture.py
  • domain/base_note.py
  • domain/scale.py
  • domain/color_shift.py
  • domain/chord.py
  • domain/dissonance.py
  • domain/transition.py
  • domain/quality.py
  • domain/ids.py
  • domain/mode_specs.py
  • domain/mode.py
  • domain/key.py

这些文件共同构成一个由低到高的理论层级:

枚举与基础集合
-> 基音
-> 音程
-> 音阶
-> 和弦与和弦质量
-> 调式
-> 调性

在这个体系中,每个对象都不是凭经验命名,而是由更基础的对象严格构造出来。


第一章 基音、音程、音阶

本章对应文件:

  • domain/enums/__init__.py
  • domain/enums/_lookup.py
  • domain/enums/core.py
  • domain/enums/harmony.py
  • domain/enums/runtime.py
  • domain/enums/texture.py
  • domain/base_note.py
  • domain/scale.py
  • domain/color_shift.py

1.1 枚举与查找规则

在本包中,最基本的理论单位并不是直接写成字符串,而是先写成枚举。
这样做的目的,是使每个理论名词同时具备:

  1. 固定取值范围;
  2. 可比较性;
  3. 可索引性;
  4. 可从值反查枚举成员的能力。

其中,LookupEnum 给出统一的查找规则。
若某个值可以合法地对应到某个枚举成员,则返回该成员;
若不能对应,则抛出错误。

因此,枚举在本包中不仅承担“命名”的作用,也承担“合法性约束”的作用。

1.2 基础集合

在本章中,先约定以下几个基础集合。

设:

  • N = {C, D, E, F, G, A, B} 为自然音名集合;
  • D = {I, II, III, IV, V, VI, VII} 为音级集合;
  • P = {0, 1, 2, ..., 11} 为十二音高类集合。

此外,还要约定若干扩展集合。

第一,形态集合:

  • V = {Base, Ascending, Descending, SubV}

第二,和声功能集合:

  • F = {Tonic, Dominant, Subdominant, Characteristic}

第三,织体集合:

  • T = {Columnar, Ascending, Triangular, Decomposition}

第四,运行时导向集合:

  • 动态类型:{Strong, Weak}
  • 状态:{Consonant, Dissonant}
  • 声部:{Bass, Tenor, Alto, Soprano}
  • 连接类型:{Step, Jump, Suspend, Transit}

由此可见,domain/enums 的任务,是为后续全部理论对象提供一套离散而稳定的语义坐标系。

1.3 音级

音级对应 Degrees
它是一个七项循环系统,而不是普通整数系统。

因此,对于任意 a, b ∈ D

  • a + b 表示在七级循环中向前推进;
  • a - b 表示在七级循环中做相对回退。

这说明,在本包中,音级的本质是“相对位置”,不是“绝对数量”。

1.4 音名

音名对应 NoteNames
每个自然音名都对应一个基础音高类:

  • C -> 0
  • D -> 2
  • E -> 4
  • F -> 5
  • G -> 7
  • A -> 9
  • B -> 11

此外,音名还和音级发生关系。

n ∈ Nd ∈ D,则:

  • n + d 表示以 n 为出发点,沿音名字母循环前进 d 级后的结果;
  • n - d 表示沿音名字母循环后退 d 级后的结果;
  • n1 | n2 表示两个音名之间的级位差。

因此,音名在本包中不是单纯标签,而是携带字母顺序结构的对象。

1.5 音程

音程对应 Intervals
一个音程由两项确定:

i = (d, p)

其中:

  • d ∈ D 表示级位差;
  • p ∈ P 表示半音差。

例如:

  • P1 = (I, 0)
  • m2 = (II, 1)
  • M2 = (II, 2)
  • m3 = (III, 3)
  • M3 = (III, 4)
  • P5 = (V, 7)

因此,音程不是先有名称再附带意义,而是先有“级位差 + 半音差”的二元结构,再由此确定名称。

1.6 基音

基音对应 BaseNote
在本包中,一个基音由下列两项构成:

t = (n, s)

其中:

  • n ∈ N
  • s 为升降记号数量,当前限制为 -2 <= s <= 2

由此定义该基音的音高类为:

pc(t) = (value(n) + s) mod 12

因此,基音同时包含:

  1. 记谱上的拼写信息;
  2. 实际上的音高类信息。

这就意味着:

  • C#Db 可以同音高类;
  • 但在本包中,它们不是同一个基音。

1.7 基音之间的关系

设有两个基音:

t1 = (n1, s1)
t2 = (n2, s2)

则从 t1t2 的关系由两部分确定:

  1. 音名之间的级位差;
  2. 音高类之间的半音差。

因此,基音对基音的运算 t1 | t2 返回一个音程。
这说明,在本包中,音程是通过两个基音的差被计算出来的,而不是凭经验直接赋名。

1.8 基音与音程的运算

t 为基音,i 为音程,则本包定义:

  • t + i
  • t - i

其意义分别是:

  • 由一个已知基音,沿音程正向构造目标基音;
  • 由一个已知基音,沿音程反向回推目标基音。

在这个过程中,系统既要保持音名字母的正确推进,也要保持目标音高类的正确落位。
若某个结果需要超过当前允许的升降记号数量,则该构造不成立。

1.9 音阶

音阶对应 Scale
一个音阶由:

  1. 一个主音;
  2. 一个长度为 7 的音程序列;

共同确定。

因此,一个音阶可以写作:

X = (t0; i1, i2, ..., i7)

其中:

  • t0 为主音;
  • i1 = P1
  • i1, ..., i7 依次给出七个级位相对主音的位置。

音阶中的第 r 级音定义为:

X[r] = t0 + ir

其中 r ∈ D

因此,音阶不是“七个音的集合”,而是“一个主音下的有序七项结构”。

1.10 七声音阶的合法性

在本包中,一个 Scale 合法,当且仅当:

  1. 音程序列长度为 7;
  2. 第一项为 P1
  3. 每一级都能在当前拼写限制下构造出合法基音。

由此可见,本包中的七声音阶既受乐理结构约束,也受拼写实现约束。

1.11 音阶与基音的关系

X 为音阶,t 为基音。
则音阶可以对基音做反查。

若存在 r ∈ D 使得:

X[r] = t

则称 t 属于该音阶,且其级位为 r
若不存在这样的 r,则称 t 不属于该音阶。

因此,本包中的“属于音阶”是严格拼写意义下的属于,而不是仅按音高类近似判断。

1.12 色彩迁移

色彩迁移对应 ColorShift
它不是和弦概念,而是音阶概念。

一个色彩迁移由三项确定:

CS = (src, diff, dst)

其中:

  • src 为起始音阶的七音程序列;
  • diff 为主音之间的音程差;
  • dst 为目标音阶的七音程序列。

因此,色彩迁移描述的是:

从一个音阶到另一个音阶时, 主音如何变化, 内部级位分布又如何变化。

它不是“调高若干半音”的单维变换,而是“主音 + 音阶结构”的联合变换。

1.13 最小用例

下面这个例子同时覆盖了:

  1. BaseNote + Intervals
  2. BaseNote | BaseNote
  3. Scale[Degrees]
from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.enums.core import Degrees, Intervals, NoteNames
from composer_theory.domain.scale import Scale

c = BaseNote(NoteNames.C)
g = c + Intervals.P5

c_major = Scale(
    tonic=c,
    intervals=(
        Intervals.P1,
        Intervals.M2,
        Intervals.M3,
        Intervals.P4,
        Intervals.P5,
        Intervals.M6,
        Intervals.M7,
    ),
)

assert g == c_major[Degrees.V]
assert (c | g) == Intervals.P5

第二章 和弦

本章对应文件:

  • domain/chord.py
  • domain/dissonance.py
  • domain/transition.py
  • domain/quality.py

2.1 和弦的基本定义

和弦对应 Chord
在本包中,一个和弦由:

  1. 一个音阶;
  2. 一个相对根音的音级集合;

共同确定。

因此,一个和弦可以写作:

C = (X, A)

其中:

  • X 为根音音阶;
  • A ⊆ D
  • I ∈ A

这里 I ∈ A 是强制条件。
因为在本包中,I 表示和弦根音本身。
若缺少 I,则这个结构不能构成合法和弦。

2.2 和弦的基音集合

C = (X, A) 为一个和弦。
则它的实际基音集合定义为:

Notes(C) = { X[r] | r ∈ A }

因此,和弦中的音不是单独逐个记录的,
而是由一个“根音音阶 + 相对音级集合”共同推导出来的。

2.3 和弦中的相对级位

在本包中,和弦内部的级位始终相对和弦根音计算。
例如:

  • I 表示根音;
  • III 表示和弦内部第三音;
  • V 表示和弦内部第五音。

这说明,Chord.composition 的坐标系不是“相对调式主音”,而是“相对和弦根音”。

2.4 和弦质量

和弦质量对应 Quality
一个和弦质量由三部分构成:

Q = (b, T, O)

其中:

  • b 为基础质量;
  • T 为张力集合;
  • O 为省略集合。

基础质量来自 Qualities,例如:

  • maj
  • min
  • dim
  • maj7
  • min7
  • dim7
  • min7b5

因此,和弦质量不是一个单独名字,而是由骨架、扩展和删减共同组成的结构。

2.5 和弦质量的反推

在本包中,Quality 通常不是手动指定,而是通过和弦内部音程集合反推得到。

其基本思想是:

  1. 先由和弦组成音得到相对根音的音程集合;
  2. 再在既有基础质量中寻找最匹配的一项;
  3. 把额外音记为张力;
  4. 把缺失音记为省略。

因此,质量判断不是任意命名,而是一个受限的最优匹配过程。

2.6 不协和关系

和弦内部的不协和关系由 DissonanceRelation 表示。
一个不协和关系至少包含:

  1. 参与该关系的音程集合;
  2. 关系类别;
  3. 优先级;
  4. 最小解决移动数;
  5. 各成员音的解决方向。

例如,当前系统会识别:

  • 小二度
  • 大二度
  • 纯四度
  • 三全音
  • 小七度
  • 大七度
  • 增三和弦音集

因此,本包中的不协和不是抽象感受,而是可列举、可评分、可推导解决方向的结构。

2.7 解决方向

不协和关系中的解决方向由 Resolution 表示。
当前允许:

  • NONE
  • STEP_UP
  • STEP_DOWN
  • STEP_EITHER

这说明,在本包中,和弦内部的张力不仅被识别,还被赋予了后续运动倾向。

2.8 张力分数

C 为一个和弦。
C.tension_score 表示它内部不协和关系累积后的总体张力强度。

该分数由规则优先级汇总后得到,并被裁剪在 010 的范围内。
因此,它不是任意评价,而是一个由规则系统生成的定量结果。

2.9 目标音倾向

C.target_note_tendencies 表示:

若和弦内部某些不协和音按规则解决,
则它们更可能导向哪些相对级位。

因此,在本包中,一个和弦不仅是静态集合,
也隐含了从自身结构推出的未来运动方向。

2.10 功能迁移

功能迁移对应 Transition
它由三项组成:

TR = (Q1, k, Q2)

其中:

  • Q1 为源和弦质量;
  • k 为根音半音差;
  • Q2 为目标和弦质量。

因此,功能迁移描述的是:

一个和弦从某种质量变到另一种质量时,
在根音位置上发生了怎样的位移。

2.11 和弦差

C1C2 为两个和弦。
则在本包中,C1 - C2 的结果不是一个和弦,而是:

(Transition, ColorShift)

这意味着:

和弦与和弦之间的差,被拆成了两部分:

  1. 和弦质量与根音位置的变化;
  2. 所属音阶结构的色彩变化。

因此,和弦变化在本包中总是被看作“功能层变化”与“色彩层变化”的联合。

2.12 最小用例

下面这个例子展示:

  1. 如何从音阶构造和弦
  2. 如何读取质量
  3. 如何读取张力分数
from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.chord import Chord
from composer_theory.domain.enums.core import Degrees, Intervals, NoteNames
from composer_theory.domain.scale import Scale

c_ionian = Scale(
    tonic=BaseNote(NoteNames.C),
    intervals=(
        Intervals.P1,
        Intervals.M2,
        Intervals.M3,
        Intervals.P4,
        Intervals.P5,
        Intervals.M6,
        Intervals.M7,
    ),
)

cmaj7 = Chord(c_ionian, frozenset({Degrees.I, Degrees.III, Degrees.V, Degrees.VII}))

assert str(cmaj7) == "Cmaj7"
assert cmaj7.quality.name == "maj7"
assert cmaj7.tension_score >= 0

第三章 调式

本章对应文件:

  • domain/ids.py
  • domain/mode_specs.py
  • domain/mode.py

3.1 调式规格

调式规格对应 ModeSpec
一个调式规格由三部分组成:

M = (name, variants, c)

其中:

  • name 为调式名称;
  • variants 为从形态到七音程序列的映射;
  • c ∈ D 为特征级。

因此,调式规格并不是一个具体实例,
而是一套“怎样构造该调式”的公共规则。

3.2 七种基础调式

当前系统的基础调式为:

  • Ionian
  • Dorian
  • Phrygian
  • Lydian
  • Mixolydian
  • Aeolian
  • Locrian

这些调式的基础形态都在 MODE_SPECS 中预先写定。
因此,调式实例化时不是重新发明音阶,而是调用既有规格。

3.3 调式形态

对于某些调式,除了基础形态 Base 外,还可能存在:

  • Ascending
  • Descending

这说明,在本包中,一个调式不是必然只对应一个音阶。
它可以对应若干相关形态,而这些形态共享同一个调式身份。

3.4 调式

调式对应 Mode
一个调式由:

  1. 一个主音;
  2. 一个调式类型;

共同确定。

因此,一个调式可写作:

mode = (t, m)

其中:

  • t 为主音;
  • m 为调式类型。

由此,调式会为其全部合法形态生成对应音阶。

3.5 调式的基础音阶

mode = (t, m)v 为某个形态。
则该调式在形态 v 下的音阶定义为:

Scale_v(mode) = (t; profile_v(m))

因此,调式中的音阶是由“主音 + 规格中的音程序列”共同确定的。

3.6 调式的派生音阶

r ∈ D
mode[RootVariantScaleRef(r, v)] 表示:

在调式 modev 形态中,
取第 r 级为新主音后形成的派生音阶。

这里的关键不在于“重新挑七个音”,
而在于“保持原级位顺序,仅改变参考主音”。

3.7 调式的和弦构造

调式可以通过索引生成和弦。
若给定:

  • 根音级位 r
  • 调式形态 v
  • 和弦组成集合 A

则调式中的和弦可写作:

Chord(mode[RootVariantScaleRef(r, v)], A)

因此,调式是和弦构造的上层背景。
和弦不是脱离调式凭空生成,而是先通过调式中的某一级音阶建立起来。

3.8 特征级

每个调式都有一个特征级 characteristic_degree
它表示该调式相对于其它调式最有辨识度的级位。

因此,在本包中,调式的个性不是模糊描述,而是通过一个明确的特征级被记录下来。

3.9 调式的大小属性

调式还具有 tonality,即大小属性。
当前实现通过基础形态中的第三音程来判断:

  • 若第三度为大三度,则视为 maj
  • 若第三度为小三度,则视为 min

因此,调式的大小属性在本包中是从结构中推导出来的,而不是额外手写标签。

3.10 调式索引与和弦编号

为了使“调式索引和弦”成为一个稳定而可复用的操作,
本包定义了 ChordId

一个 ChordId 由三部分组成:

CID = (scale_ref, A)

其中:

  • scale_ref ∈ {RootVariantScaleRef(root_degree, variant), SubVScaleRef(target_degree)}
  • A ⊆ D 为和弦内部组成级位集合,且必须包含 Degrees.I

因此,ChordId 的作用不是给和弦命名,
而是给出“如何在一个调式中构造该和弦”的索引说明。

在当前实现中,调式 mode 的公开入口有以下几类:

  1. mode[scale_ref]
  2. ChordId
  3. mode.chord(scale_ref, composition=None)

其含义分别是:

第一,若给出 mode[scale_ref]
则直接返回该 scale_ref 规范化后的目标音阶。

第二,若给出 ChordId(scale_ref, A)
则先由 mode 根据 scale_ref 解析出目标音阶,
再以该音阶和组成集合 A 构造和弦。

第三,若给出 mode.chord(scale_ref, A)
则把 scale_ref 作为显式入口送入统一的 canonical 解析流程。
A 省略,则按 Chord 的默认组成规则生成和弦。

因此,调式索引和弦的本质可以写作:

mode[scale_ref] = mode[canonical_scale_ref]
mode[ChordId(scale_ref, A)] = Chord(mode[canonical_scale_ref], A)

这说明,在本包中,调式对音阶与和弦的索引都不是查表行为,
而是一个“显式 scale_ref -> canonical scale_ref -> scale/chord”的构造过程。

3.11 最小用例

下面这个例子把调式这一章最关键的三条入口放在一起:

  1. mode[scale_ref] 入口
  2. mode.chord(scale_ref, ...) 入口
  3. canonicalization
from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.enums.core import Degrees, NoteNames
from composer_theory.domain.enums.harmony import Modes, VariantForm
from composer_theory.domain.ids import RootVariantScaleRef, SubVScaleRef
from composer_theory.domain.mode import Mode

mode = Mode(BaseNote(NoteNames.C), Modes.Dorian)

scale = mode[RootVariantScaleRef(Degrees.II, VariantForm.Base)]
triad = mode.chord(RootVariantScaleRef(Degrees.II, VariantForm.Base))
minor7 = mode.chord(
    RootVariantScaleRef(Degrees.II, VariantForm.Base),
    frozenset({Degrees.I, Degrees.III, Degrees.V, Degrees.VII}),
)
subv = mode.chord(SubVScaleRef(Degrees.VI), frozenset({Degrees.I, Degrees.III, Degrees.VII}))

assert str(scale) == "D, E, F, G, A, B, C"
assert str(triad) == "Dmin"
assert str(minor7) == "Dmin7"
assert str(mode[SubVScaleRef(Degrees.I)]) == "Db, Eb, F, Gb, Ab, Bb, Cb"

第四章 调性

本章对应文件:

  • domain/ids.py
  • domain/key.py

4.1 调性的基本定义

调性对应 Key
一个调性由两项构成:

K = (t, m0)

其中:

  • t 为主音;
  • m0 为主调式类型。

因此,调性在本包中的作用,不是重新定义全部音,
而是为调式提供一个统一的高层上下文。

4.2 调性的主调式

给定调性 K = (t, m0)
则其主调式就是以主音 t 和调式类型 m0 构造的调式。

因此,调性并不是脱离调式独立存在的对象,
而是建立在一个主调式之上的更高层结构。

4.3 调性中的同主音入口

若在调性 K 中直接用某个调式类型 m 取值,
则得到的是与主音相同、但调式类型不同的调式。

这类入口对应 ModeAccess.Substitute

因此,这一入口保留的是主音不变,只改变调式类型。

4.4 调性中的关系调式入口

若在调性 K 中用某个音级 r ∈ D 取值,
则得到的是主调式第 r 级所导出的关系调式。

这类入口对应 ModeAccess.Relative

因此,这一入口保留的是“同一主调式体系中的级位派生关系”。

4.5 调性与 SubV 的边界

当前实现中,Key 不再直接提供 SubV 入口。
Key 只负责生成普通 Mode 上下文;
SubV 被下移为 Mode 内部的一种音阶解析规则。

也就是说,当前结构更接近:

key -> mode -> scale_ref -> chord

其中 SubVScaleRef(target_degree) 的规则现在是:

  1. 先在当前 Mode 的基础音阶上取 target_degree 对应的目标音;
  2. 用该目标音上方的小二度作为 subv 主音,按当前 ModeSpec.subv_profile 构造目标 degree 的 subv 音阶;
  3. 直接返回这个结果,不再额外做第二次旋转。

也就是说,只有显式配置了 ModeSpec.subv_profile 的调式才支持 Mode[SubVScaleRef(...)]。 若某个调式未配置 subv_profile,则访问 SubVScaleRef(...) 会报“不支持 SubV”。

4.6 调性的模式空间

从结构上看,一个调性并不是单独一个调式,
而是一个允许若干入口方式的模式空间。

它至少包含:

  1. 同主音调式集合;
  2. 关系调式集合。

因此,调性的意义不在于“只给出主调”,
而在于给出一个可以枚举内部模式来源的理论环境。

4.7 调性的反查

调性还可以对模式、音阶、基音、和弦做反查。
其意义是:

给定某个对象,判断它是否可以在该调性的某个入口中被解释。

因此,调性既能正向生成模式,
也能反向判定某个对象在自身结构中的归属。

4.8 调性编号

为了使“一个调性”本身也能被结构化标识,
本包定义了 KeyId

一个 KeyId 由两部分组成:

KID = (tonic, main_mode_type)

其中:

  • tonic ∈ N
  • main_mode_type ∈ Modes

这表示:

一个调性的身份,
由主音名称与主调式类型共同确定。

因此,KeyId 的作用不是参与调性内部索引,
而是为“哪一个调性”提供一个稳定编号。

换言之:

  • KeyId 负责标识调性对象本身;
  • ModeId 负责标识调性内部的调式入口;
  • ChordId 负责标识调式内部的和弦入口。

由此可见,本包中的 ID 系统具有明确层级:

KeyId
-> ModeId
-> ChordId

这说明,编号体系和理论对象的构造层级是一致的。

4.9 调性索引与调式编号

为了使“调性索引调式”成为统一操作,
本包定义了 ModeId

一个 ModeId 由两部分组成:

MID = (role, access)

其中:

  • role ∈ Modes ∪ D
  • access ∈ {Substitute, Relative}

这里的含义是:

第一,role 表示被访问对象所依附的角色。
它可以是某个调式类型,也可以是某个音级。

第二,access 表示访问路径。
也就是说,同一个 role 必须结合访问方式,才能确定应当如何从调性生成目标调式。

在当前实现中,调性 key 可以接受以下四类索引:

  1. Modes
  2. Degrees
  3. (Degrees, ModeAccess)
  4. ModeId

其含义分别是:

第一,若用某个 Modes 成员索引,
则保持主音不变,只改变调式类型。
这对应同主音访问。

第二,若用某个 Degrees 成员索引,
则取主调式基础音阶的该级为新主音,
再按主调式与该级的关系推导目标调式类型。
这对应关系调式访问。

第三,若用 (degree, access) 索引,
则按显式访问方式生成目标调式。
其中当前特别支持:

  • (degree, Relative)

第四,若用 ModeId(role, access) 索引,
则把上述几类访问方式统一编码成一个结构化编号。

因此,调性索引调式的本质不是“从容器中取现成对象”,
而是:

key[ModeId(role, access)] = 按访问规则构造的 Mode

这说明,在本包中,ModeId 承担的是“调性内部模式入口编号”的作用。

4.10 最小用例

下面这个例子展示调性的三种常用索引方式:

from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.enums.core import Degrees, NoteNames
from composer_theory.domain.enums.harmony import ModeAccess, Modes
from composer_theory.domain.ids import ModeId
from composer_theory.domain.key import Key

key = Key(BaseNote(NoteNames.C), Modes.Ionian)

c_dorian = key[Modes.Dorian]
d_dorian = key[Degrees.II]
same_as_type = key[ModeId(role=Modes.Dorian, access=ModeAccess.Substitute)]

assert str(c_dorian) == "C-Dorian"
assert str(d_dorian) == "D-Dorian"
assert same_as_type == c_dorian

关系解析篇

本篇对应下列代码文件:

  • relations/__init__.py
  • relations/hit.py
  • relations/tools.py
  • relations/chord_in_mode.py
  • relations/mode_in_key.py
  • relations/chord_in_key.py
  • resolve/resolver.py

本篇只讨论公开接口。
所谓公开接口,是指:

  1. relations.__all__ 中导出的关系命中对象;
  2. 各命中对象上不以下划线开头的属性与方法;
  3. Resolver.resolve(...) 这一统一解析入口。

因此,本篇不把 _ensure_*_color_shift_altered_degrees 一类内部计算函数当作文档主体。

从结构上说,关系层的任务不是“重新定义和弦、调式、调性”,
而是回答以下三类问题:

  1. 一个和弦如何被某个调式解释;
  2. 一个调式如何被某个调性解释;
  3. 一个和弦如何被某个调性解释。

第五章 关系层的公开对象

5.1 导出集合

composer_theory.relations 当前公开导出四个名称:

  • ResolveHit
  • ChordInModeHit
  • ModeInKeyHit
  • ChordInKeyHit

因此,关系层对外的基本单位不是“布尔判断”,
而是“命中对象”。

5.2 命中的一般形式

xy 为两个乐理对象。
若系统能够把 xy 的上下文中加以解释,则返回一个命中对象 hit

因此,在本包中,解析结果不写作:

True / False

而写作:

hit = 某种 ResolveHit 子类

这表示:
系统关注的不是“能不能解释”这一件事,
而是“以什么身份被解释、附带哪些分析结果、属于哪一种关系类型”。

5.3 命中的共同特征

所有命中对象都继承自 ResolveHit
当前 ResolveHit 自身不提供专门字段,
它的作用是:

  1. 作为关系命中对象的共同父类型;
  2. 提供统一的字符串表示入口。

因此,ResolveHit 在本包中是一个抽象上的类型边界,
而不是一个承载分析值的实体对象。

5.4 公开属性与内部属性的边界

关系层对象上的公开属性,表示用户可以直接依赖的结果。
例如:

  • is_member
  • function_scores
  • chromatic_score
  • color

而以下划线开头的成员,表示内部推导过程。
这些成员只用于:

  1. 延迟计算;
  2. 缓存;
  3. 中间结构变换;
  4. 支撑公开结果。

因此,在使用关系层时,应把公开属性理解为“结果层”,
把私有成员理解为“实现层”。

5.5 最小用例

关系层最简单的读取方式,不是直接 new 某个 hit,
而是交给 Resolver 返回合适的命中对象:

from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.enums.core import Degrees, NoteNames
from composer_theory.domain.enums.harmony import Modes
from composer_theory.domain.key import Key
from composer_theory.relations import ModeInKeyHit
from composer_theory.resolve.resolver import Resolver

key = Key(BaseNote(NoteNames.C), Modes.Ionian)
hits = Resolver().resolve(key[Degrees.II], key)

assert hits
assert isinstance(hits[0], ModeInKeyHit)
assert hits[0].is_member is True

第六章 ChordInModeHit

6.1 所表示的关系

ChordInModeHit 表示:

Chord ∈ Mode

也就是说,它描述的是:
一个和弦如何在某个调式中被解释。

6.2 基本字段

一个 ChordInModeHit 由以下公开字段确定:

  • mode
  • chord_id
  • chord

其中:

  • mode 表示被解释所依附的调式;
  • chord_id 表示该和弦若属于此调式,则它在该调式中的身份;
  • chord 表示被分析的实际和弦对象。

因此,一个 ChordInModeHit 的核心不是“这个和弦本身是什么”,
而是“这个和弦在此调式里被看作什么”。

6.3 成员命中与非成员命中

hit 为一个 ChordInModeHit
则:

  • hit.chord_id is not None 时,称该命中为成员命中;
  • hit.chord_id is None 时,称该命中为非成员命中。

公开属性 is_member 正是对此事实的直接表达。

因此,在本包中,和弦对调式的解析并不只允许“命中或空”。
即使该和弦不属于该调式,也可以返回一个非成员分析命中。

6.4 和弦身份

hit.chord_id 存在,则它包含三项:

  • scale_ref
  • 和弦内部组成 composition

其中 scale_ref 可能有两种形式:

  • RootVariantScaleRef(root_degree, variant)
  • SubVScaleRef(target_degree)

这两项共同决定:

该和弦在此调式中,
是按普通 root+variant 方式解释,还是按 SubV 方式解释,
并由哪些相对级位构成。

因此,chord_id 不是“和弦名字”,
而是调式内部的一种定位坐标。

6.5 功能分数

hit.function_scores 返回一个从和声功能到分数的映射:

Functions -> float

当前至少包括:

  • Tonic
  • Dominant
  • Subdominant

其意义是:
把该和弦的组成音,放到当前调式主音为参考点的坐标下,
统计它对各功能证据的匹配程度。

因此,function_scores 不是和弦固有属性,
而是“和弦相对当前调式”的功能分析结果。

6.6 半音化分数

hit.chromatic_score 表示该和弦相对当前调式基础形态所产生的半音化程度。

若该命中是:

  1. 非成员命中,则统计和弦中有多少音不属于该调式基础音阶;
  2. 基础形态成员命中,则分数为 0
  3. 非基础形态成员命中,则统计该和弦涉及的级位中,有多少级相对基础形态发生了偏移。

因此,chromatic_score 描述的是该和弦相对调式基础骨架的离调程度。

6.7 色彩结果

hit.color 的结果为:

(Transition, ColorShift)

其中:

  • 第一项描述和弦质量与根音的变化;
  • 第二项描述所属音阶结构的色彩变化。

因此,在关系层中,色彩结果不是单独一个标签,
而是和弦差与音阶差的联合表达。

6.8 转折点

hit.turning_points() 返回一个集合。
它只在以下条件下可能非空:

  1. 当前命中是成员命中;
  2. 当前命中的 scale_refRootVariantScaleRef
  3. 当前命中的 variant 不是 Base
  4. 该和弦涉及到与 VIVII 相关的关注级位。

因此,转折点不是每个和弦都天然携带的标签,
而是特定 root+variant 形态下由级位组合触发的运行时结果。
SubVScaleRef 命中默认不产生 turning point。

6.9 字符串表示

str(hit) 会给出一条面向阅读的关系说明。
若为非成员命中,则强调其是 non-member analysis
若为成员命中,则会写出根级、组成级与形态信息。

因此,字符串表示的作用不是替代字段访问,
而是把一条命中压缩成可快速浏览的说明文本。

6.10 最小用例

下面这个例子展示一个标准成员命中:

from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.enums.core import Degrees, NoteNames
from composer_theory.domain.enums.harmony import Modes, VariantForm
from composer_theory.domain.ids import ChordId, RootVariantScaleRef
from composer_theory.domain.mode import Mode
from composer_theory.relations import ChordInModeHit
from composer_theory.resolve.resolver import Resolver

mode = Mode(BaseNote(NoteNames.C), Modes.Ionian)
chord = mode[
    ChordId(
        RootVariantScaleRef(Degrees.V, VariantForm.Base),
        frozenset({Degrees.I, Degrees.III, Degrees.V, Degrees.VII}),
    )
]
hit = Resolver().resolve(chord, mode)[0]

assert isinstance(hit, ChordInModeHit)
assert hit.is_member is True
assert hit.chord_id.scale_ref == RootVariantScaleRef(Degrees.V, VariantForm.Base)
assert hit.turning_points() == set()

第七章 ModeInKeyHit

7.1 所表示的关系

ModeInKeyHit 表示:

Mode ∈ Key

也就是说,它描述的是:
一个调式如何在某个调性中被解释。

7.2 基本字段

一个 ModeInKeyHit 由以下公开字段确定:

  • key
  • mode_id
  • mode
  • is_member

其中:

  • key 表示被解释所依附的调性;
  • mode_id 表示该调式在调性中的入口身份;
  • mode 表示被命中的具体调式对象。

因此,ModeInKeyHit 所回答的问题是:
这个调式在该调性中属于哪一种入口,而不是单纯地“是否相等”。

7.3 成员命中与直接分析命中

hit 为一个 ModeInKeyHit
则:

  • hit.mode_id is not None 时,称该命中为成员命中;
  • hit.mode_id is None 时,称该命中为直接分析命中。

公开属性 is_member 正是对此事实的直接表达。

因此,在当前实现中,调式对调性的解析也允许回退分析。
即使一个调式不能作为该调性的明确入口被命中,
系统仍可以返回一个 ModeInKeyHit 来承载功能与色彩分析结果。

7.4 调式身份

mode_id 由两部分组成:

  • access
  • role

其中:

  • access 指示入口类型;
  • role 指示该入口所依附的角色。

当前入口类型包括:

  • Substitute
  • Relative

因此,调式在调性中的身份并不只有一种。
同一个调式可以通过不同入口被纳入同一调性环境。

7.5 功能分数

hit.function_scores 返回:

Functions -> float

它所分析的对象不是该调式中的任意和弦,
而是该调式主和弦骨架在主调式参考系下的功能证据。

因此,这个分数描述的是:
该调式一旦被放入此调性,
其骨架重心更偏向主功能、属功能还是下属功能。

7.6 半音化分数

hit.chromatic_score 表示:

相对于主调式基础形态,
该调式基础形态中共有多少个音级发生了变音。

因此,这个量不是统计“有几个外来音实例”,
而是统计“有几个结构级位与主调骨架不同”。

若该命中不是成员命中,
则当前实现改为直接比较:

  1. 当前调式基础音阶的七个级位;
  2. 主调式基础音阶的音高类集合。

此时它表示的是:
该调式基础音阶中,有多少个级位的音高类不属于主调式音高集合。

7.7 色彩结果

hit.color 仍写作:

(Transition, ColorShift)

但这里所比较的对象不是任意和弦,
而是:

  1. 当前调式的骨架主和弦;
  2. 调性主调式的主和弦。

因此,ModeInKeyHit.color 表达的是:

当一个调式被纳入某个调性时,
它相对于主调中心所带来的功能色彩位移。

7.8 字符串表示

str(hit) 会写出:

  • 调式名;
  • 入口类型;
  • 入口角色;
  • 所属调性。

若该命中不是成员命中,则会写成该调式在某调性中的直接分析。

因此,字符串表示提供的是一条“调式在调性中的归属句子”。

7.9 最小用例

下面这个例子展示一个关系调式命中:

from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.enums.core import Degrees, NoteNames
from composer_theory.domain.enums.harmony import ModeAccess, Modes
from composer_theory.domain.ids import ModeId
from composer_theory.domain.key import Key
from composer_theory.relations import ModeInKeyHit
from composer_theory.resolve.resolver import Resolver

key = Key(BaseNote(NoteNames.C), Modes.Ionian)
mode = key[Degrees.II]
hit = Resolver().resolve(mode, key)[0]

assert isinstance(hit, ModeInKeyHit)
assert hit.is_member is True
assert hit.mode_id == ModeId(role=Degrees.II, access=ModeAccess.Relative)
assert hit.mode == mode

第八章 ChordInKeyHit

8.1 所表示的关系

ChordInKeyHit 表示:

Chord ∈ Key

也就是说,它描述的是:
一个和弦如何在某个调性中被解释。

8.2 复合结构

ChordInKeyHit 不是从零开始的单层关系。
它内部包含:

  • 一个 ModeInKeyHit
  • 一个可能存在的 ChordId
  • 一个实际和弦 chord

因此,它本质上是两层解释的叠加:

Chord ∈ Mode ∈ Key

8.3 公开字段

一个 ChordInKeyHit 当前公开以下核心字段与属性:

  • mode_in_key_hit
  • chord_id
  • chord
  • is_member
  • function_scores
  • chromatic_score
  • color

这说明,ChordInKeyHit 对外保留的是完整复合关系本身,
而不再额外摊平内部上下文。

8.4 调式成员性

is_member 表示:

该和弦是否至少可以作为某个调式内部和弦,
从而经由该调式进入当前调性。

因此,它不是“是否属于这个调性”的绝对布尔值,
而是“是否属于当前解释链条中的调式成员”。

8.5 上下文读取方式

若需要读取该命中所依附的调性与调式上下文,
则应显式通过 mode_in_key_hit 进入。

例如,应当读取:

  • hit.mode_in_key_hit.key
  • hit.mode_in_key_hit.mode
  • hit.mode_in_key_hit.mode_id

因此,ChordInKeyHit 不再把这些上下文信息额外摊平成自身属性。

8.6 功能分数

hit.function_scores 返回一个功能分数字典。
它的参考系不是当前被命中的调式主音,
而是当前调性的主调式主音。

因此,这个分数所回答的问题是:

该和弦若放在整个调性中心下看,
更像主、属还是下属。

8.7 半音化分数

hit.chromatic_score 当前按主调式基础音阶统计外来音高类数量。

也就是说,系统先取当前调性的主调式基础音阶,
再看该和弦中有多少音高类不在其中。

因此,ChordInKeyHit 的半音化分数关注的是:

该和弦相对“主调中心”的离调程度,
而不是相对其局部命中调式的离调程度。

8.8 色彩结果

hit.color 仍返回:

(Transition, ColorShift)

但其比较基准是:

  1. 当前和弦;
  2. 当前调性主调式的主和弦。

因此,它描述的是:
该和弦相对于调性中心主和弦的综合位移。

8.9 成员命中与直接分析命中

ChordInModeHit 类似,
ChordInKeyHit 也允许 chord_id 为空。

chord_id is None 时,
表示系统没有找到一个具体的调式内成员身份,
但仍然返回一个基于当前调性的直接分析命中。

因此,在本包中,Chord ∈ Key 的解析也不是简单的“命中或空”,
而保留了一种调性层面的回退分析结果。

8.10 字符串表示

str(hit) 会在成员命中时写出:

  • 调式
  • 入口类型
  • 入口角色
  • 普通命中时的 root/variant,或 SubV 命中时的 target
  • 组成级

若为直接分析命中,则直接写成该和弦在某调性中的直接分析。

因此,字符串表示压缩的是一条完整的复合解释路径。

8.11 最小用例

下面这个例子展示一个典型的 Chord ∈ Key 成员命中:

from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.enums.core import Degrees, NoteNames
from composer_theory.domain.enums.harmony import ModeAccess, Modes, VariantForm
from composer_theory.domain.ids import ChordId, RootVariantScaleRef
from composer_theory.domain.key import Key
from composer_theory.relations import ChordInKeyHit
from composer_theory.resolve.resolver import Resolver

key = Key(BaseNote(NoteNames.C), Modes.Ionian)
mode = key[Degrees.I]
chord = mode[
    ChordId(
        RootVariantScaleRef(Degrees.V, VariantForm.Base),
        frozenset({Degrees.I, Degrees.III, Degrees.V, Degrees.VII}),
    )
]
hit = Resolver().resolve(chord, key)[0]

assert isinstance(hit, ChordInKeyHit)
assert hit.is_member is True
assert hit.mode_in_key_hit.mode_id.access == ModeAccess.Substitute
assert hit.chord_id.scale_ref == RootVariantScaleRef(Degrees.V, VariantForm.Base)

第九章 Resolver

9.1 统一解析入口

Resolver 的公开入口是:

resolve(a, b) -> List[AnyResolveHit]

其中 AnyResolveHit 是以下三类命中的并集:

  • ChordInModeHit
  • ModeInKeyHit
  • ChordInKeyHit

因此,解析器的任务不是返回单个固定类型,
而是根据输入对象对,返回对应关系域中的命中列表。

9.2 支持的输入对

当前解析器支持三类输入:

  1. Chord, Mode
  2. Mode, Key
  3. Chord, Key

若用户输入顺序反过来,
解析器会在内部判断是否可以交换次序后再继续处理。

因此,解析器对这三类关系采用的是“有向关系定义 + 有限交换兼容”的策略。

9.3 Chord -> Mode 的解析

当输入为 ChordMode 时,
解析器会遍历该调式的全部可用 scale_ref 候选。

若某一候选满足:

  1. scale_ref 所解析出的音阶可以承载当前和弦组成;
  2. 构造出的候选和弦与输入和弦具有相同基音集合;

则生成一个 ChordInModeHit

当前枚举的候选包括:

  1. 各个 RootVariantScaleRef(root_degree, variant)
  2. 各个 SubVScaleRef(target_degree)

命中后还会经过 canonicalization 去重。

若所有形态都失败,
则仍返回一个 ChordInModeHit,但令:

  • chord_id = None
  • chord = 原始和弦

因此,Chord -> Mode 解析始终至少返回一个分析结果。

9.4 Mode -> Key 的解析

当输入为 ModeKey 时,
解析器会依次检查:

  1. 同主音入口;
  2. 关系调式入口。

凡是能与输入调式相等者,都生成一个 ModeInKeyHit

若没有任何入口命中,
则仍返回一个 ModeInKeyHit,但令:

  • mode_id = None
  • mode = 原始调式

因此,Mode -> Key 现在也保留了回退分析机制。
它与另外两条解析链一样,都会至少返回一个分析结果。

9.5 Chord -> Key 的解析

当输入为 ChordKey 时,
解析器会先枚举该调性的全部模式入口。

对于每一个模式入口,
它先构造 ModeInKeyHit
再尝试把该和弦精确解析为该模式中的成员和弦。
每当精确命中成功,就生成一个 ChordInKeyHit

若全部入口都不能得到成员命中,
则解析器仍返回一个 ChordInKeyHit,其中:

  • chord_id = None
  • chord = 原始和弦

同时,它仍附带一个默认的 ModeInKeyHit 作为调性上下文。

因此,Chord -> Key 也保留了回退分析机制。

9.6 返回值的意义

解析器返回的是列表,而不是单个命中。
这是因为:

  1. 一个对象可能通过多个入口被解释;
  2. 一个和弦可能在多个形态中都成立;
  3. 一个调式可能以多种方式纳入同一调性。

因此,解析结果天然允许多义性,
而不是强制压缩为唯一答案。

9.7 最小用例

下面这个例子把三条解析链放在一起:

from composer_theory.domain.base_note import BaseNote
from composer_theory.domain.enums.core import Degrees, NoteNames
from composer_theory.domain.enums.harmony import Modes, VariantForm
from composer_theory.domain.ids import ChordId, RootVariantScaleRef
from composer_theory.domain.key import Key
from composer_theory.relations import ChordInKeyHit, ChordInModeHit, ModeInKeyHit
from composer_theory.resolve.resolver import Resolver

resolver = Resolver()
key = Key(BaseNote(NoteNames.C), Modes.Ionian)
mode = key[Degrees.I]
chord = mode[
    ChordId(
        RootVariantScaleRef(Degrees.V, VariantForm.Base),
        frozenset({Degrees.I, Degrees.III, Degrees.V, Degrees.VII}),
    )
]

hits_chord_mode = resolver.resolve(chord, mode)
hits_mode_key = resolver.resolve(key[Degrees.II], key)
hits_chord_key = resolver.resolve(chord, key)

assert any(isinstance(hit, ChordInModeHit) for hit in hits_chord_mode)
assert any(isinstance(hit, ModeInKeyHit) for hit in hits_mode_key)
assert any(isinstance(hit, ChordInKeyHit) for hit in hits_chord_key)

本文档小结

本文档由两篇组成。

第一篇说明 domain 层的基础乐理定义。
第二篇说明 relationsresolve 层如何把和弦、调式、调性组织成可解析的关系系统。

在关系层中,系统不直接返回真假值,
而是返回命中对象。
每个命中对象都同时携带:

  1. 关系身份;
  2. 上下文对象;
  3. 功能分析结果;
  4. 半音化结果;
  5. 色彩结果。

在解析层中,Resolver 统一负责把对象对映射成命中列表。
其中:

  • Chord -> Mode 有非成员回退分析;
  • Mode -> Key 有直接分析回退;
  • Chord -> Key 有调性层回退分析。

因此,composer_theory 在当前结构下,不只是一个静态乐理对象集合,
也是一个可以对“和弦、调式、调性之间的解释关系”进行形式化求解的系统。

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

composer_theory-0.1.0.tar.gz (75.1 kB view details)

Uploaded Source

Built Distribution

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

composer_theory-0.1.0-py3-none-any.whl (71.0 kB view details)

Uploaded Python 3

File details

Details for the file composer_theory-0.1.0.tar.gz.

File metadata

  • Download URL: composer_theory-0.1.0.tar.gz
  • Upload date:
  • Size: 75.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for composer_theory-0.1.0.tar.gz
Algorithm Hash digest
SHA256 55888c408f178aa77c7b21f9662e2ffe9bf5f422c94f52a024f9b721199fa5d6
MD5 72610ebf375243dde35179bb5ecbd5d1
BLAKE2b-256 d9d48369f93abd0ab11d0c821a4f758ba7a7afe63e384f0fc894f384eead31ea

See more details on using hashes here.

File details

Details for the file composer_theory-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for composer_theory-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2fbf8e76d620b2c9ba168023ea3044ba80deb8f999600be5fd69327afd131bb0
MD5 81f04d2407e3ca95875c4265abf9d3e4
BLAKE2b-256 89bbfad5725060c023cc238ab261156415ce33063072a19e0361e2a8b6d36ed8

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