【日常篇】002_五线谱调式推导

五线谱调式推导

  很早的时候就听说过各种诸如“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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值