带有快速枚举的简单音乐音调值
我从事的乐器应用程序通常包含音乐理论部分。 这可能是一个简单的大尺度或更复杂的算法。 通常,我最终只使用MIDI音符编号来获取音高值。
结果最终看起来像:
let cMajor = [48, 50, 52, 53, 55, 57, 59]
我决定做一些工作来制作一个可以在项目中重复使用的简单模式,以使代码更易于阅读。
代码很短,所以我将从此开始:
enum Pitch: Int {
case C = 0, Cs, D, Ds, E, F, Fs, G, Gs, A, As, B
}
现在我可以写:
let cMajor :[Pitch] = [.C,.D,.E,.F,.G,.A,.B]
[Pitch]
的显式类型允许我只键入.G
而不是Pitch.G
,我认为它看起来更好。
概念
理想情况下,我将能够将音高作为偶然的音符名称来编写。 这些值看起来就像C♯
或D♭
。 这些值将是类型安全的,并且像原始类型一样起作用,如果我错误地输入了假间距,则会导致编译器抛出错误。
设计决策
当我经历一些选择时,我意识到我将不得不做出一些妥协。
没有Unicode意外情况: ♯
和♭
不在Swift支持的Unicode范围内。 有替代符号,但默认字体未包含这些替代符号,因此代码可移植性成为一个问题。 我为竖琴选择了一个简单s
后缀。
没有枚举别名: Swift枚举不支持重复值或别名。 我不能优雅地将C♯
设置为等于D♭
。 我选择保留一切。 在我的脑海里,我称之为“调谐器格式”,因为我在吉他调音器上的剪辑仅在其LCD屏幕上使用了锐器。
最近的音高
音高名称的另一个重要特征是它们是圆形的。 即使B
在列表的相对两端,也仅是C
一步。
我创建了一种简单的方法来查找最接近的音高。
extension Pitch {
func nearest(_ destination: Pitch) -> Int {
let right = (12 - self.rawValue + destination.rawValue) % 12
let left = -(self.rawValue + 12 - destination.rawValue) % 12
return abs(right) <= abs(left) ? right : left
}
}
贪婪地向右看,向左看该值,然后将其减小到八度以下,因为音高不超过12步。
这样一来,值就可以以半音为单位计算第二个音高的距离和方向。 在使用中,它看起来像这样:
Pitch.C.nearest(.B) // -1
Pitch.B.nearest(.C) // 1
Pitch.C.nearest(.D) // 2
Pitch.C.nearest(.G) // -5
实际使用
自从搬到圣路易斯以来,我一直在演奏班卓琴。 我经常忘记的一件事是如何移入和移出备用调音。 使用此值类型和方法,可以很容易地用代码进行演示。
我最常使用的两个调音是开G和双C。表示如下:
var openG :[Pitch] = [.G,.D,.G,.B,.D]
var doubleC :[Pitch] = [.G,.C,.G,.C,.D]
现在,如果我想从空G移到双C,我仅使用几种功能方法:
let adjust = zip(openG, doubleC).map{$0.nearest($1)}
这将获取openG中的每个音高,将其与doubleC中的相应音高配对,然后计算音高之间的距离。
这样产生:
[0, 1, 0, -2, 0]
这意味着第二个字符串上升1步,第四个字符串下降2。
而且我也可以将其反转以从双C转到G:
let adjust = zip(doubleC, openG).map{$0.nearest($1)}
[0, -1, 0, 2, 0]
扩展使用
音高名称实际上只是一个基础。 我可以想到很多非常有趣的应用程序,例如实现算术,MIDI映射,比例过滤器,键签名和字符串便捷方法。
实际上,可以将一些简单的行粘贴到项目中,并让以后的开发人员轻松理解。