使用 Swift 编写的乐谱符号 API。它的编写方式使其可以在大多数操作系统中使用,例如 iOS、macOS、tvOS。目前,Windows 和 Linux 是期望的目标,但由于计划避免所有依赖项,因此应该不会太困难。创建此库的目标是几乎零依赖。
music-notation
旨在实现模型层和控制器层,它们了解乐谱符号的工作原理。它将没有渲染功能,也没有输入/输出。这些功能将在附加软件包中实现。
请参阅 music-notation-render
、music-notation-io
以及 music-notation-import
。
目标是将这些作为基于 Swift Package Manager 的软件包提供。将不再支持 Cocoapods 和 Carthage。仍然支持手动将软件包添加到 Xcode 项目中。
请参考此 Swift 风格指南,了解此仓库中使用的编码风格指南,并确保遵守它们。
请注意,有关最新要求,请参阅 Package.swift
可以使用 Swift Package Manager 或手动安装 music-notation
。
Swift Package Manager 需要 Swift 3.0 或更高版本,但库本身需要 Swift 6。首先,创建一个 Package.swift
文件。它应该如下所示
dependencies: [
.package(url: "https://github.com/music-notation-swift/music-notation.git", from: "0.2.9")
]
然后,swift build
应该会拉取并编译 music-notation
以供您开始使用。
您还可以直接在 Xcode 项目中安装 SPM 软件包。
要手动安装,您需要克隆 music-notation
存储库。您可以在单独的目录中执行此操作,也可以使用 git 子模块。完成后,您只需将 Sources/music-notation
文件夹中的文件拖放到您的项目中。如果您使用文件夹引用而不是组,那么每次从源存储库中提取时,您都会获得任何新添加的文件,而无需确定是否添加了新文件然后手动添加它们。
注意:如果您以 iOS 7 为目标,则必须手动安装,因为嵌入式框架需要 iOS 8 或 OSX Mavericks 的最低部署目标。
WIP (正在进行中) music-notation-import 当前是该软件包的第一个真实客户端和用法示例。它尚未在 github 上,但是一旦开始工作,我将选择片段在此处展示。
WIP (正在进行中) 由于许多具体实现将在软件包中进行,因此这将是配置的主要途径。
WIP (正在进行中)
为了描述音乐以及此库建模的声部,需要一些定义。这将告知建模这些组件的类和结构体的名称。
总谱 (Score) 是用乐谱符号描述的一首乐曲,其中包含一个或多个声部 (Parts)。它还包含与整个总谱相关的描述。
示例包括
这是一个(部分)总谱的示例(贝多芬 - 第 9 交响曲 Op. 125,从 Musescore.com 下载)。
在示例中,您可以看到总谱的元素以及构成每个单独声部的谱表。
声部 (Part) 在总谱中对单个乐器进行建模,因此描述了乐器的属性以及特定于声部的数据,包括一个或多个谱表 (Staves)。
示例包括
以下是一些声部的示例。在这种情况下,它们都是为说明目的而制作的总谱的钢琴声部。
乐谱符号的本质是由一个或多个谱表 (Staves) 表示的。一个谱表由五条水平线组成,称为谱线
每个谱表通常包含一个旋律,通常由单个乐器演奏。旋律由一系列要演奏的音调组成。每个音调都是一种声音,具有音高和时值,而演奏旋律的音乐家应该发出与这些音调相对应的声音。
一个音符由一个符头、一个连接到符头的可选符干以及一个连接到符干的可选符尾或符杠组成。这些属性决定了音调的时值。
一个全音符的时值等于十六个十六分音符的时值,依此类推。音乐的速度,即节奏,在整个总谱中是恒定的。这意味着总谱中的每个全音符都需要花费相同的时间来演奏;通常在 1 到 4 秒之间。
符头的垂直位置定义了音调的频率或音高。每个音高都用一个 a 到 g 之间的字母命名,后跟多个 ′ 符号;每个 ′ 符号表示频率乘以二。每条谱线代表一个音高。通常,中间线上的音高为 b′;这通过在谱表的左边缘绘制谱号来表示。使用其他谱号,谱线代表其他音高。
在谱表的上半部分排版的音符通常是倒置排版的。这纯粹是排版上的决定,不会影响音乐的语义。
可以使用休止符来表示沉默。休止符的作用类似于音符,除了音乐家在休止符的持续时间内保持沉默。休止符用特殊符号表示
休止符的垂直位置没有意义,与音符不同,休止符不能倒置排版。
音符按顺序从左到右演奏。时间被分成相等持续时间的小节,由小节线分隔。每个小节的持续时间由拍号定义,拍号表示全音符的一部分以及这些音符被强调的节拍。在标记新的拍号之前,所有以下小节都具有相同的拍号。
当同时演奏多个音符时,可能会因为音符作为和弦演奏(和声,由可以同时演奏多个音符的乐器演奏,例如钢琴)而发生。当两个乐器在不同的声部中演奏音符,甚至当它们从同一声部中的不同谱表中演奏时,也可能发生。
总谱通常由几个连接的谱表组成,每个谱表包含由一个乐器演奏的音乐。同时演奏的音符总是写在相同的水平位置
不仅总谱使用几个连接的谱表:例如,钢琴音乐通常使用两个谱表,分别包含左手和右手的音符。这也可以在吉他音乐中看到,吉他音乐需要两个谱表才能准确演奏。一个标准记谱谱表和一个六线谱谱表。
有时,一个乐器的多个独立的声部出现在一个谱表中;在这种情况下,符干方向决定了哪些音符应该由哪个乐器演奏。带有向上和向下符干的音符通常分别称为上声部和下声部。“声部”一词起源于合唱音乐,这种记谱法很常见。许多软件记谱编辑器支持 4 个声部,并且在编辑过程中使用颜色来区分每个声部。
如上所述,单个乐器也可以演奏和弦,和弦由许多同时发出的音调组成。这是通过在同一符干上添加多个符头来表示的
声乐音乐(例如歌曲)可以使用乐谱符号来记谱。这是通过在代表要演唱的旋律的谱表下方编写歌曲文本或歌词来完成的。每个音节都写在它所属的音符的正下方
乐谱符号有多种谱号,用于指示谱线代表哪些音符。它们都有一个符号,其中一个点被指定为音符选择。
例如,高音谱号是一个风格化的字母 G,G 的卷曲部分表示 G 音符在谱线上的位置。请参见下面风格化的 G 指向 G 谱线的位置(注意:这是一个法国小提琴高音谱号)。
低音谱号类似,代表风格化的字母 F,两个点围绕 F 的谱线。
中音谱号不类似,但代表指向相关谱表的风格化方式。
这是其中一些谱号在谱表上进行比较。
拍号(也称为 meter signature
, metre signature
, 或 measure signature
)是一种乐谱记号,用于指定每个小节(bar)包含多少个节拍(脉冲),以及哪个音符时值相当于一个节拍。
在乐谱中,拍号出现在乐谱的开头,以时间符号或堆叠的数字表示,例如 common time(通用拍号)或 3/4
(分别读作 common time 和 four-分之三 拍),紧随调号(如果调号为空,则紧随谱号)。乐谱中间出现的拍号,通常紧随小节线之后,表示节拍的改变。
拍号有多种类型,具体取决于音乐是否遵循规则的(或对称的)节拍模式,包括简单拍号(例如,3/4
和 4/4
)和复拍号(例如,9/8
和 12/8
);或者涉及移动的节拍模式,包括混合拍号(例如,5/4
或 7/8
)、奇数拍号(例如,5/8
& 3/8
或 6/8
& 3/4
)、加成拍号(例如,3 + 2/8 + 3
)、分数拍号(例如,2½/4
)和复杂节拍(例如,3/10
或 5/24
)。
规则的 (
.simple
,.compound
) 拍号是指每个小节代表 2、3 或 4 个主节拍的拍号。
- 二拍子 指的是每个小节有 2 个主节拍
- 三拍子 指的是每个小节有 3 个主节拍
- 四拍子 指的是每个小节有 4 个主节拍
.simple
拍号的主节拍分为两个一级子节拍。.compound
拍号的主节拍分为三个一级子节拍。在
.simple
和.compound
拍号中,二级子节拍总是被分成两部分(永远不会分成三部分)。
节拍涉及多个脉冲层协同工作以按时间组织音乐的方式。 西方音乐中的标准节拍可以分为简单节拍和复节拍,以及二拍子、三拍子和四拍子。
二拍子、三拍子和四拍子的分类是根据计数脉冲和比计数脉冲慢的脉冲之间的关系得出的。 换句话说,这是一个分组问题:每个小节出现多少个节拍。
如果计数脉冲节拍分为两组,则我们有二拍子; 三个一组,三拍子; 四个一组,四拍子。 指挥模式是根据这些分类确定的。
简单和复合分类源于计数脉冲和比计数脉冲快的脉冲之间的关系。换句话说,这是一个分割问题:每个节拍是否分为两个相等的部分,或三个相等的部分。 将节拍分为两个相等部分的节拍是简单节拍; 将节拍分为三个相等部分的节拍是复节拍。
因此,西方音乐中有六种标准节拍
在拍号中,顶部的数字(只有顶部的数字!)描述了节拍的类型。 以下是始终与每种节拍类型相对应的顶部的数字
据我所知,仅从拍号(尤其是其奇数性)几乎无法推断出有关节拍的信息。
例如,8/8
是一个奇数节拍,计为 1-2-3, 2-2-3, 3-2,而 4/4 将是 1 &, 2 &, 3 &, 4 &。
然而,9/8
是一個偶數拍 (1-2-3, 2-2-3, 3-2-3)
在简单节拍中,拍号的底部数字对应于与单个节拍相对应的音符类型。 如果一个简单节拍被标记为每个四分音符对应一个节拍,则拍号的底部数字为 4。如果一个简单节拍被标记为每个二分音符对应一个节拍,则拍号的底部数字为 2。如果一个简单节拍被标记为每个八分音符对应一个节拍,则拍号的底部数字为 8。依此类推。
在复节拍中,拍号的底部数字对应于与单个节拍分割相对应的音符类型。 如果一个复节拍被标记为每个附点四分音符对应一个节拍,则八分音符是节拍的分割,因此拍号的底部数字为 8。
如果一个复节拍被标记为每个附点二分音符对应一个节拍,则四分音符是节拍的分割,因此拍号的底部数字为 4。 请注意,因为在一个复节拍中,节拍被分成三部分,所以节拍总是分割音符的三倍长,并且节拍总是附点的。
music-notation
和填充特定组件的库基于早期工作,可以在以下位置找到
music-notation
和附加程序包首先设计为 Swift Package Manager 程序包,因此不会提供 Xcode 项目。
一个值得注意的例外是 music-notation-import,它是一个 macOS 命令行实用程序,用于解析各种其他格式并将它们转换为 music-notation
数据结构。 Import 主要是程序包的使用者,这解释了选择 Xcode 项目的原因。
为了避免与代码格式发生任何冲突,已经提供了完整的样式指南以及项目范围内的 lint 设置。 请使用基本的 music-notation
设置,并将它们完整地复制到任何子程序包中。
每个项目都应复制 Github Actions 的格式:build & test
、code coverage
,以及 linting
徽章,以便开发人员可以一目了然地看到项目是否处于良好状态。
music-notation
以及子程序包使用 Github Actions 作为持续集成系统。 请注意提供的 workflows
。 每个贡献存储库应至少具有以下操作
build-test.yml 此操作检出代码并构建它,以及运行测试。 徽章会出现在存储库上,指示 Pass
/Fail
。
llvm-cov.yml
此操作检出代码并在捕获代码覆盖率的同时运行测试。 徽章会出现在存储库上,指示覆盖率百分比。
swiftlint.yml
此操作使用存储库的 .swiftlint.yml
设置在代码上运行 swiftlint
。 这些设置在每个程序包中都应相同,并且构成 music-notation
系列中所有程序包和项目的基本代码设置。 徽章会出现在存储库上,指示 Pass
/Fail
。
由于 music-notation
允许以任何旧顺序(在 API 约束范围内)输入音符,并且对这些音符的音乐解释需要某种时间顺序,那么应该采取哪条路径?
我们是在添加每个音符时按时间顺序对其进行排序,还是提供 API 以实现按时间顺序排列的正确排序。
如果采取后一种方式,我们是否以提供的粒度提供排序?
例如,我们是否提供一个 API,该 API 提供每个 score
、part
、measure
、staff
、voice
中按顺序排列的所有音符?
目前,按需音符流是 music-notation
的目标路径。 它将提供最佳路径,以满足目标受众的渲染和语义分析需求。
作者:Kyle Sherman, Miguel Osorio
在音乐中,可以同时演奏具有 2 个不同音高的音符。 你会看到一个符干,并且从该符干上延伸出 2 个位于不同谱线/谱间上的符头。 通过使每个 Note
都有一个 SpelledPitch
es 数组已经完成了此操作。 但是,与此同时,您还可以同时演奏持续时间不同的音符。 在本文档中,我将此称为小节中的第二组音符。
这种情况在架子鼓音乐中经常发生。 在 4/4 的标准摇滚节拍中,您会看到踩镲在整个小节中演奏八分音符。 军鼓在第 2 和第 4 拍上演奏八分音符,可以在同时具有军鼓和踩镲的音符上表示为 2 个音高。 然后,您会在第 1 和第 3 拍上看到低音鼓作为四分音符,并在其间使用四分音符休止符。
小节中的第二组音符可以出现
我们采用现在拥有的 [NoteCollection]
数组并将其设为二维:[[NoteCollection]]
。 每组音符将表示为最外层数组中的一个元素。
原因 - 我们可以完全独立地对最外层数组的每个元素执行小节持续时间验证。 - 同时渲染到每个子数组中,以便它可以轻松知道音符是否在时间切片中彼此对齐。 - API 可以更改为仅具有一个可以默认为 0 的集合索引。
我们没有对此进行深入的研究,因为这似乎会增加很多复杂性。 这尤其会使得索引到小节中以执行任何变化功能变得困难。 因此,我们搁置了这一点。
我们认为我们可能能够拥有一个协议类型的单维数组,该协议类型表示垂直时间切片。 这样,您将拥有一个简单的单维数组。 并且您将在某种表示单个时间切片的协议中表示多个音符。
主要问题是这将使小节持续时间验证变得非常困难。 插入也将变得非常困难,因为您将必须弄清楚是否需要在两个现有时间切片之间插入新的时间切片,或者这是否与已存在的时间切片对齐。
作者: Kyle Sherman
此设计涵盖了一个实现细节,该细节仅与此库的维护者和开发人员相关,而与 API 的用户无关。
在这个库中实现多个结构体时,使用了一种技术,允许同时以两种不同的方式索引容器内的元素。一种索引访问存储信息的结构体,另一种索引用于以用户看到的方式(扁平化)访问数据。
Staff
包含一个 NotesHolder
数组。NotesHolder
是一个协议,Measure
和 MeasureRepeat
都实现了这个协议。因此,Staff 包含一个数组,其中每个元素要么是一个 Measure
,要么是一个存储一个或多个重复 Measure
数据的结构(MeasureRepeat
)。
对于像 Staff.insertMeasure(at:beforeRepeat)
这样的方法,传递给 at
参数的索引将考虑到展开的 MeasureRepeat
,包括其重复的小节。因此,为了进行这种索引,我们需要能够将该索引转换为 Staff
存储的 NotesHolder
数组中的索引,如果指定索引处存在 MeasureRepeat
,则还需要转换为 MeasureRepeat
。
我们有一个名为 recompute<Name>Indexes
的方法,该方法在需要计算这组辅助索引的数组的 didSet
中被调用。 对于 Staff
,该方法称为 recomputeMeasureIndexes
。 在此方法中,我们循环遍历数组并获取任何嵌套结构的索引并展开这些索引。 结果数据结构是一个元组数组。 该元组具有 2 个元素:主索引和辅助索引。 辅助索引是可选的,因为它仅在索引处存在嵌套数据结构时使用。
Staff.recomputeMeasureIndexes()
将设置 measureIndexes
数组。 此数组将存储 (notesHolderIndex: Int, repeatMeasureIndex: Int?)
类型的元组。
recompute<Name>Indexes
将循环遍历数组并向索引数组添加一个元组条目。 如果遇到嵌套结构,它将执行嵌套循环,并为该嵌套结构中的每个元素添加一个元组条目。
因此,该算法的最坏情况效率为 O(n*m)
,其中 n
是主数组中元素的数量,m
是嵌套结构中元素的数量。 当然,一段音乐不会包含所有的嵌套结构。
为了限制对性能的影响,此方法仅在修改数组时执行。 其思想是修改数组的次数将少于调用非变异方法的次数。 但是,尚未对此进行充分的探索或测试。 尚未对性能进行专门测量。
开放性问题:如果有人能想到更有效的方法,请告诉我们。
开放性问题:这似乎可以使其通用,而不是一遍又一遍地编写非常相似的代码。 但是,我目前无法想到一种方便的方法来实现这一点,尽管我还没有对此进行过深入的思考。
Staff
中用于 NotesHolder
数组。Measure
中用于 NoteCollection
数组。Tuplet
中用于 NoteCollection
数组。作者: Kyle Sherman
大多数时候你会看到 3、5、6、7 或 9 的分组。但是,如果你的拍号的上方数字是奇数,你也可以看到 2、4 或 8 的分组。此外,大多数关于连音符的资料都会列出以下分组:
但它们都以等等结尾。因此,我认为我们不应该验证这个数字。 看起来它可以是任何任意数字。
一个完整的连音符定义需要知道多少个什么时值的音符适合多少个某个时值的音符的空间。 这就是你定义比率的方式。
你可以在 这里 看到一个简单明了的对话框。
通常,两个音符的时值是相同的,但是你可以使用任何时值进行计算。 例如,你通常会在 4/4 拍中将一个标准八分音符三连音定义为“3 个八分音符占据 2 个八分音符的空间”。 但是,你也可以将其定义为“3 个八分音符占据 1 个四分音符的空间”。 这里的比率将是 3:2。
某些比率是标准的,例如三连音通常具有 3:2 的比率。 但是,也存在非标准比率,你可以随意使用,例如 8:5。 最好定义标准比率,以便例如用户可以创建三连音而不必定义完整的比率。
只需要列出 2-9 的标准比率。 在这种情况下,我们可以将它们列在 Tuplet
结构体中。 这样,如果在初始化器中第二个数字不是特定的,并且第一个数字是其中之一,我们可以根据默认比率的静态列表填写第二个数字。
开放性问题:每个数字可以有一个标准比率吗? 似乎该标准可能基于拍号。 如果不是,我们可以使用以下标准比率。
目前假设这些是标准比率
2: 3
3: 2
4: 3
5: 4
6: 4
7: 4
8: 6
9: 8
你可以拥有一个由另一个连音符组成的连音符,而该连音符又由另一个连音符组成,依此类推。
为了支持此行为,Tuplet
将存储一个 NoteCollection
类型的数组,以便它可以将 Note
或 Tuplet
作为数组中的值。 此外,NoteCollection
将必须具有一个属性或一组属性来传达每个 NoteCollection
的时值和该时值的数量。
即,3:2 的八分音符连音符将具有 2 的数量和 .eighth
的时值。 单个八分音符将具有 1 的数量和 .eighth
的时值
连音符需要始终是满的。 这意味着如果是 3:2 的比率,则需要包含 3 个音符。 但是,这些音符可以是休止符。 因此,唯一的变异函数应该是替换音符,而不是移除或添加。
你也可以拥有不同时值的音符作为连音符的一部分,如 这里 所示,其中你有带有八分音符和延音四分音符的三连音。
由于这些规则,必须在初始化器中进行某种类型的验证,以确保给定的音符等于一个完整的连音符。 这必须内置于 Tuplet
结构体中。
开放性问题:命名仍在争论中。 遇到麻烦了。 我们做了第一遍,似乎还不错。
struct Tuplet: NoteCollection {
/// The notes that make up the tuplet
public private(set) var notes: [NoteCollection]
/// The number of notes of the specified duration that this tuplet contains
public let noteCount: Int
/// The duration of the notes that define this tuplet
public let noteDuration: NoteDuration
/// The number of notes that this tuplet fits in the space of
public let noteTimingCount: Int
init(_ count: Int, _ baseNoteDuration: NoteDuration, inSpaceOf baseCount: Int? = nil, notes: [NoteCollection]) throws
mutating func replaceNote<T: NoteCollection>(at index: Int, with noteCollection: T) throws
mutating func replaceNote<T: NoteCollection>(at index: Int, with noteCollections: [T]) throws
mutating func replaceNotes<T: NoteCollection>(in range: CountableClosedRange<Int>, with noteCollections: [T])
mutating func replaceNotes<T: NoteCollection>(in range: CountableClosedRange<Int>, with noteCollection: T) throws
}
我们可以选择像 Finale 中那样指定第二个时值。 但是,为了将比率放在顶部,是否需要转换为相同的时值? 因此,我决定删除第二个时值的选择。
-开放性问题:为什么要设置第二个时值? 我们是否需要转换为相同的时值以获得比率?-
我删除了此功能,因为它似乎没有用。
如果确实有第二个时值,则初始化程序将以以下方式运行
如果要使用标准比率,则只能指定第一个数字和时值。 然后,Tuplet
将验证指定的数字是否为标准(2-9),如果是,则会自动将第二个数字设置为静态比率。
标准比率
let standardTriplet = try! Tuplet(3, .eighth, notes: [eighthNote, eighthNote, eighthNote])
带有奇数定义的标准比率
let standardTriplet = try! Tuplet(3, .eighth, inSpaceOf: 1, .quarter, notes: [eighthNote, eighthNote, eighthNote])
自定义比率
let customOctuplet = try! Tuplet(
8,
.eighth,
inSpaceOf: 5,
.eighth,
notes: [eighthNote, eighthNote, eighthNote, eighthNote, eighthNote, eighthNote, eighthNote, eighthNote]
)
我们需要使用与其他地方类似的方法来拥有一组扩展的索引来获取每个音符。 请参阅 设计文档。 这是必需的,因为可以存在复合连音符,并且我们希望能够用 Note
或 Tuplet
替换单个音符。 因此,即使音符位于复合 Tuplet
中,我们也需要能够索引到单个音符中。
Tuplet
将拥有自己的索引,而 Measure
将为 Tuplet
获取用于其目的的索引。
这个有点不同,因为你可以拥有包含 Tuplet
的 Tuplet
。
开放性问题:仍在弄清楚如何表示这种情况。
之前的 API
protocol NoteCollection {
var noteCount: Int
}
之后的 API
开放性问题:命名仍在争论中。 需要与上面的 Tuplet
属性命名匹配。
protocol NoteCollection {
/**
The duration of the note that in combination with `noteTimingCount`
will give you the amount of time this `NoteCollection` occupies.
*/
var noteDuration: NoteDuration
/**
The number of notes to indicate the amount of time occupied by this
`NoteCollection`. Combine this with `noteDuration`.
*/
var noteTimingCount: Int
/// The count of actual notes in this `NoteCollection`
var noteCount: Int
}
它曾经是一个枚举,并且点是音符上的属性。 但是,你可以创建一个连音符,其基本音符为附点音符。 因此,将 NoteDuration
结合值(八分音符、四分音符等)与点的数量是有意义的。 这也很有意义,因为这两个属性的组合决定了音符的长度。
现在,Tuplet
将能够拥有一个 NoteDuration
,它将完整地描述基本音符类型。
之前的 API
public enum NoteDuration: Int {
case whole = 1
case half = 2
case quarter = 4
// ...
}
之后的 API
public struct NoteDuration {
public enum Value {
case long
case large
case doubleWhole
case whole
case half
// ...
}
public let value: Value
public let dotCount: Int
public var timeSignatureValue: Int? {
switch value {
case whole: return 1
case half: 2
// ...
case .long, .large, .doubleWhole: return nil
}
}
private init(value: Value)
public init(value: Value, dotCount: Int) throws
public static let long = NoteDuration(value: .long)
public static let large = NoteDuration(value: .large)
// ...
}
有关贡献回 music-notation
的指南,请参阅 CONTRIBUTING。 这可能有点早,但是一旦事情开始正常工作,请随时提问。
music-notation
在 MIT 许可下发布。 有关详细信息,请参阅 LICENSE。
http://www2.siba.fi/muste1/index.php?id=100&la=en https://usermanuals.finalemusic.com/Finale2014Mac/Content/Finale/TPDLG.htm https://usermanuals.finalemusic.com/Finale2014Mac/Content/Finale/SIMPLETUPLETS.htm https://musescore.org/en/handbook/tuplet http://www.rpmseattle.com/of_note/create-a-tuplet-of-any-ratio-in-sibelius/
WIP 这是文档归属指针的放置位置。
一些基本音乐理论的描述来自和/或改编自:• Separating input language and formatter in GNU Lilypond, Erik Sandberg ersa9195@student.uu.se Master’s Thesis / Examensarbete NV3, 20 credits