五线谱调式推导
很早的时候就听说过各种诸如“X大调”、“X小调”这样的术语,但听了这么多年也没有搞明白这究竟是什么意思。
直到最近两个月,才有大佬提示,其实各种调式升降记号都是可以由钢琴键位平移的方式得到的,这提供了一种推断调式的方法:在最常规的C大调基础上,每个音都提升一个音程,然后再看有哪些音跑到了黑键上,就可以知道有几个升号或降号了。
尽管这个过程用手算也是很容易完成的,但考虑到自己在C++公选结课后,已经有两年多的时间没有碰过C++了。为了再次熟练这个比较重要的语言,本次推导过程将使用C++来完成。
基本思路
从C大调出发
C大调在五线谱中,是没有任何升降号的存在的:


而如果将键位整体右移两个半音,则可以得到两个升号的调(D大调):


因为一个八度共有12个半音,所以这样的平移一共可以得到十二种调式。只要对C大调的键位进行12次平移,就可以得到全部的十二种调式。
调式在程序中的表示
一个八度有12个半音,而在推导过程中需要不断地将这些键往一个方向进行平移,因此考虑到循环移位。使用12个bit分别对应一个八度中的12个半音C、C#|Db、D、D#|Eb、E、F、F#|Gb、G、G#|Ab、A、A#|Bb、B,若键位在那个半音上则置1,否则置0。每一次往高音处移位时,超出B的部分都将会回到C的位置。
让第0位表示C,第1位表示C#……以此类推,第11位表示B,如果向左移位的话,则第11位被移动至第0位:
例如,C大调对应的七个键位分别为C、D、E、F、G、A、B,则对应的bit序列为:101010110101。而向高音处平移两个半音到D大调,则对应向左循环移位两次,得到:101011010110,对应C#、D、E、F#、G、A、B。
有了比较明确的思路后,就可以开始写代码了。
代码实现
初始状态:C大调
前面已经提到,C大调的bit表示方法,而初始状态又确认为C大调,因此这个数值可以作为常量存放在头文件中:
int MODE = 0xAB5; //101010110101 <- reverse(101011010101)
位与音符的对应关系
类似于python的字典,C++的标准库也有提供字典这个结构。该结构存放在map库中,include这个库即可用std::map进行使用(这里已经用了using std::map,所以在使用时直接写的map):
map<int, string> NOTE_MAP = {{0, "C",},{1, "C#",},{2, "D",},{3, "D#",},
{4, "E",},{5, "F",},{6, "F#",},{7, "G",},
{8, "G#",},{9, "A",},{10, "A#",},{11, "B",},
{12, "C",},{13, "Db",},{14, "D",},{15, "Eb",},
{16, "E",},{17, "F",},{18, "Gb",},{19, "G",},
{20, "Ab",},{21, "A",},{22, "Bb",},{23, "B",}};
在这里给了24个词条,主要是因为有些调式是使用降号的,对于这样的调式,应该使用降号表示法(对应字典的第13-24条)。
循环移位
循环移位的思路非常简单,就是向左移位一次,然后将最高位移动到第一位。这里没有对第12位及以上的bit进行截断,但也没有影响,因为后续的操作不会涉及到更高位的bit:
/*
* brief 对调式向左(高音方向)移动一个单位
*
* @param orgMode 原有的调式
*/
void shiftMode(int &orgMode){
orgMode <<= 1;
orgMode = orgMode | !!((1 << 12) & orgMode);
}
打印调式信息
调式有升号调的和降号调这两种,为了区分开来,在这里使用降号调的flag标记:dFlag,来将它们区分开来。如果有降号表示,则会在字典中查询第13-24项的词条:
/*
* brief 打印调式的相关信息
*
* @param mode 调式
* @param dFlag 降号表示,默认为0,即升号
*/
void printModeInfo(int &mode, bool dFlag = false){
for (int i = 0; i < 12; i++){
int mask = (1 << i);
if (mode & mask){
std::cout << NOTE_MAP[i+12*dFlag] << "\t";
}
}
std::cout << std::endl;
}
main函数
对每一次移位的结果,都进行打印即可。由于先前已有经验,升号调和降号调是交替出现的,所以在这里就可以很容易地判断出dFlag的值是true还是false:
int main()
{
int mapSize = NOTE_MAP.size();
int shift = 0;
do{
printModeInfo(MODE, (((shift <= 5) && (shift % 2)) || ((shift >= 8) && !(shift % 2))) ? true : false);
shiftMode(MODE);
shift += 1;
}
while (shift < 12);
return 0;
}
运行结果及结论
运行后得到如下输出:
C D E F G A B
C Db Eb F Gb Ab Bb
C# D E F# G A B
C D Eb F G Ab Bb
C# D# E F# G# A B
C D E F G A Bb
C# D# F F# G# A# B
C D E F# G A B
C Db Eb F G Ab Bb
C# D E F# G# A B
C D Eb F G A Bb
C# D# E F# G# A# B
此即推导得到的12种调式,注意到升号和降号的数量是关于C大调对称的,用图像表示则更为直观:
沿着这个图像顺时针看,调式依次从C大调、降D大调、D大调……演变到B大调,最后再回到C大调。而这些大调,每一个都又有着相对应的小调,由于还没有找出规律,就不在这里描述了。
而对于C大调向上移动6个半音的调式——升F大调,则暂时难以在输出结果中得以理解。
而经过在overture和网上的查询,得知升F大调由六个升记号或六个降记号组成:


并且,E#=F,因为E和F之间只差一个半音。
由此一来,调式这个问题也就得到了一个比较清晰的解答。
完整代码
main.h
#include <map>
#include <string>
using std::map;
using std::string;
map<int, string> NOTE_MAP = {{0, "C",},{1, "C#",},{2, "D",},{3, "D#",},
{4, "E",},{5, "F",},{6, "F#",},{7, "G",},
{8, "G#",},{9, "A",},{10, "A#",},{11, "B",},
{12, "C",},{13, "Db",},{14, "D",},{15, "Eb",},
{16, "E",},{17, "F",},{18, "Gb",},{19, "G",},
{20, "Ab",},{21, "A",},{22, "Bb",},{23, "B",}};
int MODE = 0xAB5; //101010110101 <- reverse(101011010101)
main.cpp
#include <iostream>
#include <string>
#include "main.h"
using std::map;
using std::string;
void shiftMode(int &orgMode);
void printModeInfo(int &mode, bool dFlag);
int main()
{
int mapSize = NOTE_MAP.size();
int shift = 0;
do{
printModeInfo(MODE, (((shift <= 5) && (shift % 2)) || ((shift >= 8) && !(shift % 2))) ? true : false);
shiftMode(MODE);
shift += 1;
}
while (shift < 12);
return 0;
}
/*
* brief 对调式向左(高音方向)移动一个单位
*
* @param orgMode 原有的调式
*/
void shiftMode(int &orgMode){
orgMode <<= 1;
orgMode = orgMode | !!((1 << 12) & orgMode);
}
/*
* brief 打印调式的相关信息
*
* @param mode 调式
* @param dFlag 降号表示,默认为0,即升号
*/
void printModeInfo(int &mode, bool dFlag = false){
for (int i = 0; i < 12; i++){
int mask = (1 << i);
if (mode & mask){
std::cout << NOTE_MAP[i+12*dFlag] << "\t";
}
}
std::cout << std::endl;
}