用 200 行 Python 代码掌握基本音乐理论

共 30977字,需浏览 62分钟

 ·

2021-04-29 21:19

本文作者是一位多年自学成才的吉他手,但对西方乐理一无所知,因此决定编写一些代码来搞懂它。
本文用了大约200行Python代码来帮助我们理解西方音乐理论的基础知识。
我们将首先查看西方音乐理论中的音符,使用它们来导出给定键中的半音阶,然后将其与音程公式结合起来以导出常见的音阶和和弦。
最后,我们将研究模式,这些模式是从通用音阶衍生出来的整个音阶集合,可以用来唤起比主要音阶和次要音阶所提供的悲喜二分法更微妙的情绪和气氛。
十二音符
西方音乐的音乐字母由字母A到G组成,它们代表不同的音高。
我们可以使用以下Python列表来表示音乐字母:
alphabet = ['A''B''C''D''E''F''G']
但是,这些音符的频率分布不均。为了使音高之间的间距更均匀,我们有以下十二个音符:
notes_basic = [
    ['A'],
    ['A#''Bb'],
    ['B'],
    ['C'],
    ['C#''Db'],
    ['D'],
    ['D#''Eb'],
    ['E'],
    ['F'],
    ['F#''Gb'],
    ['G'],
    ['G#''Ab'],
]
这里有四点要注意:首先,每个音符相距半步或半音,其次,它的表示方式是通过可选的尾随符号(称为偶然符号)来表示半步升高(尖锐, ♯)或将基调降低半个音阶(平坦,♭),第三,上述音符只是循环并重新开始,但是音阶较高。
最后,您会注意到,其中一些音符由包含多个名称的列表表示:这些是谐音等效词,这是一种奇特的说法,即同一音符可以具有不同的“拼写”。因此,例如,音符在A上方半步是A♯,但也可以认为是B下方半步的音符,因此可以称为B♭。由于历史原因,音符 B/C 和 E/F 之间没有尖锐或平坦的部分。
我们需要这些等效词的重要原因是,当我们开始推导通用音阶(大,小和模式)时,连续音符必须以连续字母开头。具有不同字母的谐音等效词使我们能够正确得出这些音阶。
实际上,对于某些键,上述谐音音符是不够的。为了满足“连续字母的不同字母规则”,我们最终不得不使用双尖锐和双扁平方式来提高或降低音符整步。这些音阶通常具有不需要这些双重偶然性的等效词,但是为了完整起见,我们可以通过重写我们的注释来包括所有可能的谐音等效词,如下所示:
notes = [
    ['B#',  'C',  'Dbb'],
    ['B##''C#''Db'],
    ['C##''D',  'Ebb'],
    ['D#',  'Eb''Fbb'],
    ['D##''E',  'Fb'],
    ['E#',  'F',  'Gbb'],
    ['E##''F#''Gb'],
    ['F##''G',  'Abb'],
    ['G#',  'Ab'],
    ['G##''A',  'Bbb'],
    ['A#',  'Bb''Cbb'],
    ['A##''B',  'Cb'],
]
半音音阶
半音阶是最简单的音阶,它仅由给定音调(音阶中的主要音符,也称为音调)的八度之间的所有(十二个)半音组成。
我们可以很容易地为任何给定的键生成一个半音阶:(i)在我们的笔记列表中找到该音符的索引,(ii)向左旋转音符列表多次。
查找给定音符的索引
让我们编写一个简单的函数来在此列表中查找特定的音符:
def find_note_index(scale, search_note):
    ''' Given a scale, find the index of a particular note '''
    for index, note in enumerate(scale):
        # Deal with situations where we have a list of enharmonic
        # equivalents, as well as just a single note as and str.
        if type(note) == list:
            if search_note in note:
                return index
        elif type(note) == str:
            if search_note == note:
                return index
find_note_index()函数将一系列音符(scale)和要搜索的音符(search_note)作为参数,并通过简单的线性搜索返回索引。我们在循环中处理两种情况:(i)提供的音阶由单个音符组成(例如上面的字母列表),或(ii)由音阶等效音列表组成(例如上面的notenotes_basic列表)。下面是该函数对于这两种情况的示例:
>>> find_note_index(notes, 'A')    # notes is a list of lists
9
>>> find_note_index(alphabet, 'A'# alphabet is a list of notes
0
向左旋转音符
现在,我们可以编写一个将给定scale旋转n步的函数:
def rotate(scale, n):
    ''' Left-rotate a scale by n positions. '''
    return scale[n:] + scale[:n]
我们在位置n处切割scale列表,并交换这两半。这是将alphabet列表旋转三个位置(将音符D放在前面)的示例:
>>> alphabet
['A', 'B', 'C', 'D', 'E', 'F', 'G']
>>> rotate(alphabet, 3)
['D', 'E', 'F', 'G', 'A', 'B', 'C']
在给定键中生成半音音阶
现在,我们终于可以编写我们的colour()函数了,该函数通过旋转notes数组为给定的键生成一个半音音阶:
def chromatic(key):
    ''' Generate a chromatic scale in a given key. '''
    # Figure out how much to rotate the notes list by and return
    # the rotated version.
    num_rotations = find_note_index(notes, key)
    return rotate(notes, num_rotations)
上面的colour()函数在注释列表中找到所提供键的索引(使用我们的find_note_index()函数),然后将其旋转该量以使其移到最前面(使用我们的rotate()函数)。这是生成D半音音阶的示例:
>>> import pprint
>>> pprint.pprint(chromatic('D'))
[['C##', 'D', 'Ebb'],
 ['D#', 'Eb', 'Fbb'],
 ['D##', 'E', 'Fb'],
 ['E#', 'F', 'Gbb'],
 ['E##', 'F#', 'Gb'],
 ['F##', 'G', 'Abb'],
 ['G#', 'Ab'],
 ['G##', 'A', 'Bbb'],
 ['A#', 'Bb', 'Cbb'],
 ['A##', 'B', 'Cb'],
 ['B#', 'C', 'Dbb'],
 ['B##', 'C#', 'Db']]
对于半音音阶,通常在上升时使用锐利度,而在下降时使用平坦度。但是,就目前而言,我们将谐音等值保持不变。我们将看到如何选择正确的音节以供以后使用。
间隔时间
间隔指定音符之间的相对距离。
因此,可以基于半音阶音符与根音的相对距离来命名。以下是每个音节的标准名称,其顺序与音节列表中的索引相同:
intervals = [
    ['P1''d2'],  # Perfect unison   Diminished second
    ['m2''A1'],  # Minor second     Augmented unison
    ['M2''d3'],  # Major second     Diminished third
    ['m3''A2'],  # Minor third      Augmented second
    ['M3''d4'],  # Major third      Diminished fourth
    ['P4''A3'],  # Perfect fourth   Augmented third
    ['d5''A4'],  # Diminished fifth Augmented fourth
    ['P5''d6'],  # Perfect fifth    Diminished sixth
    ['m6''A5'],  # Minor sixth      Augmented fifth
    ['M6''d7'],  # Major sixth      Diminished seventh
    ['m7''A6'],  # Minor seventh    Augmented sixth
    ['M7''d8'],  # Major seventh    Diminished octave
    ['P8''A7'],  # Perfect octave   Augmented seventh
]
同样,同一音符可以具有不同的音程名称。例如,根音可以被认为是完美的统一音色或减弱的第二音符。
从谐音等效中选取音符
给定键中的半音音阶和上述数组中的间隔,我们可以指出要使用的确切音符(并从一组谐音等效项中过滤掉)。让我们看一下执行此操作的基本方法。
举例来说,让我们看一下如何从D色阶中找到与M3或主要的第三音阶相对应的音符。
1、从区间数组中,我们可以看到找到M3的索引为4。即'M3' in intervals[4] == True
2、现在,我们在D半音音阶(以其长度为模)中查看相同的索引。我们发现colour('D')[4]是音符['E ##','F#','Gb']的列表。
3、M3中的数字(即3)表示我们需要使用的字母,其中1表示根字母。因此,例如,对于D的键,1 = D,2 = E,3 = F,4 = G,5 = A,6 = B,7 = C,8 = D…等等。因此,我们需要在包含字母F的音节列表(['E ##','F#','Gb'])中寻找一个音节。这就是音节F#
4、结论:相对于D的三分之一(M3)是F#
以编程方式标记给定键的间隔
我们可以编写一个相对简单的函数,以编程方式为我们应用此逻辑,并为我们提供一个字典,将给定键中的所有音程名称映射到正确的音符名称:
def make_intervals_standard(key):
    # Our labeled set of notes mapping interval names to notes
    labels = {}
    
    # Step 1: Generate a chromatic scale in our desired key
    chromatic_scale = chromatic(key)
    
    # The alphabets starting at provided key's alphabet
    alphabet_key = rotate(alphabet, find_note_index(alphabet, key[0]))
    
    # Iterate through all intervals (list of lists)
    for index, interval_list in enumerate(intervals):
    
        # Step 2: Find the notes to search through based on degree
        notes_to_search = chromatic_scale[index % len(chromatic_scale)]
        
        for interval_name in interval_list:
            # Get the interval degree
            degree = int(interval_name[1]) - 1 # e.g. M3 --> 3, m7 --> 7
            
            # Get the alphabet to look for
            alphabet_to_search = alphabet_key[degree % len(alphabet_key)]
            
            try:
                note = [x for x in notes_to_search if x[0] == alphabet_to_search][0]
            except:
                note = notes_to_search[0]
            
            labels[interval_name] = note

    return labels
这是我们返回C键的字典:
>>> import pprint
>>> pprint.pprint(make_intervals_standard('C'), sort_dicts=False)
{'P1': 'C',
 'd2': 'Dbb',
 'm2': 'Db',
 'A1': 'C#',
 'M2': 'D',
 'd3': 'Ebb',
 'm3': 'Eb',
 'A2': 'D#',
 'M3': 'E',
 'd4': 'Fb',
 'P4': 'F',
 'A3': 'E#',
 'd5': 'Gb',
 'A4': 'F#',
 'P5': 'G',
 'd6': 'Abb',
 'm6': 'Ab',
 'A5': 'G#',
 'M6': 'A',
 'd7': 'Bbb',
 'm7': 'Bb',
 'A6': 'A#',
 'M7': 'B',
 'd8': 'Cb',
 'P8': 'C',
 'A7': 'B#'}
间隔公式
现在,我们可以使用间隔名称指定公式或音节组,并能够将它们映射到我们想要的任何键:
def make_formula(formula, labeled):
    '''
    Given a comma-separated interval formula, and a set of labeled
    notes in a key, return the notes of the formula.
    '''

    return [labeled[x] for x in formula.split(',')]
大音阶公式
例如,大音阶的公式为:
formula = 'P1,M2,M3,P4,P5,M6,M7,P8'
我们可以使用它轻松地为不同的键生成主音阶,如下所示:
>>> for key in alphabet:
>>>     print(key, make_formula(formula, make_intervals_standard(key)))
C ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C']
D ['D', 'E', 'F#', 'G', 'A', 'B', 'C#', 'D']
E ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#', 'E']
F ['F', 'G', 'A', 'Bb', 'C', 'D', 'E', 'F']
G ['G', 'A', 'B', 'C', 'D', 'E', 'F#', 'G']
A ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#', 'A']
B ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#', 'B']
美化音阶
我们还快速编写一个更好的方法来打印音阶的函数:
def dump(scale, separator=' '):
    '''
    Pretty-print the notes of a scale. Replaces b and # characters
    for unicode flat and sharp symbols.
    '''

    return separator.join(['{:<3s}'.format(x) for x in scale]) \
                    .replace('b''\u266d') \
                    .replace('#''\u266f')
这是使用正确的unicode字符的更好输出:
>>> for key in alphabet:
>>>     scale = make_formula(formula, make_intervals_standard(key))
>>>     print('{}: {}'.format(key, dump(scale)))
C: C   D   E   F   G   A   B   C
D: D   E   F♯  G   A   B   C♯  D
E: E   F♯  G♯  A   B   C♯  D♯  E
F: F   G   A   B♭  C   D   E   F
G: G   A   B   C   D   E   F♯  G
A: A   B   C♯  D   E   F♯  G♯  A
B: B   C♯  D♯  E   F♯  G♯  A♯  B
对公式使用大音阶区间
公式命名的另一种方法是基于主要标准的音节。弹奏乐器时这会更容易,因为如果您熟悉其主要音阶,则可以在给定的琴键中获得音阶和和弦。
以下是相对于给定键中主音阶的音程名称:
intervals_major = [
    [ '1''bb2'],
    ['b2',  '#1'],
    [ '2''bb3',   '9'],
    ['b3',  '#2'],
    [ '3',  'b4'],
    [ '4',  '#3',  '11'],
    ['b5',  '#4''#11'],
    [ '5''bb6'],
    ['b6',  '#5'],
    [ '6''bb7',  '13'],
    ['b7',  '#6'],
    [ '7',  'b8'],
    [ '8',  '#7'],
]
我还添加了用于更复杂的和弦(第9、11和13)的常用音程。这些本质上是围绕模八进行包装的。因此,例如,第9位只是第2位,但高了八度。
我们还可以修改我们的make_intervals()函数以使用此函数:
def make_intervals(key, interval_type='standard'):
    ...
    for index, interval_list in enumerate(intervals):
        ...
        intervs = intervals if interval_type == 'standard' else intervals_major
        for interval_name in intervs:
            # Get the interval degree
            if interval_type == 'standard':
                degree = int(interval_name[1]) - 1 # e.g. M3 --> 3, m7 --> 7
            elif interval_type == 'major':
                degree = int(re.sub('[b#]''', interval_name)) - 1
            ...
    return labels
上面,我们刚刚向make_intervals()函数添加了一个新参数(interval_type),并在内部循环中以不同的方式计算degree度数。如果将interval_type指定为'major',则只需删除所有b字符,然后再转换为整数以获取度数即可。
推导通用音阶和和弦
这是一堆涵盖最常见音阶和和弦的公式:
formulas = {
    # Scale formulas
    'scales': {
        # Major scale, its modes, and minor scale
        'major':              '1,2,3,4,5,6,7',
        'minor':              '1,2,b3,4,5,b6,b7',
        # Melodic minor and its modes
        'melodic_minor':      '1,2,b3,4,5,6,7',
        # Harmonic minor and its modes
        'harmonic_minor':     '1,2,b3,4,5,b6,7',
        # Blues scales
        'major_blues':        '1,2,b3,3,5,6',
        'minor_blues':        '1,b3,4,b5,5,b7',
        # Penatatonic scales
        'pentatonic_major':   '1,2,3,5,6',
        'pentatonic_minor':   '1,b3,4,5,b7',
        'pentatonic_blues':   '1,b3,4,b5,5,b7',
    },
    'chords': {
        # Major
        'major':              '1,3,5',
        'major_6':            '1,3,5,6',
        'major_6_9':          '1,3,5,6,9',
        'major_7':            '1,3,5,7',
        'major_9':            '1,3,5,7,9',
        'major_13':           '1,3,5,7,9,11,13',
        'major_7_#11':        '1,3,5,7,#11',
        # Minor
        'minor':              '1,b3,5',
        'minor_6':            '1,b3,5,6',
        'minor_6_9':          '1,b3,5,6,9',
        'minor_7':            '1,b3,5,b7',
        'minor_9':            '1,b3,5,b7,9',
        'minor_11':           '1,b3,5,b7,9,11',
        'minor_7_b5':         '1,b3,b5,b7',
        # Dominant
        'dominant_7':         '1,3,5,b7',
        'dominant_9':         '1,3,5,b7,9',
        'dominant_11':        '1,3,5,b7,9,11',
        'dominant_13':        '1,3,5,b7,9,11,13',
        'dominant_7_#11':     '1,3,5,b7,#11',
        # Diminished
        'diminished':         '1,b3,b5',
        'diminished_7':       '1,b3,b5,bb7',
        'diminished_7_half':  '1,b3,b5,b7',
        # Augmented
        'augmented':          '1,3,#5',
        # Suspended
        'sus2':               '1,2,5',
        'sus4':               '1,4,5',
        '7sus2':              '1,2,5,b7',
        '7sus4':              '1,4,5,b7',
    },
}

这是在C键中生成所有这些音阶和和弦时的输出:

intervs = make_intervals('C''major')
for ftype in formulas:
    print(ftype)
    for name, formula in formulas[ftype].items():
        v = make_formula(formula, intervs)
        print('\t{}: {}'.format(name, dump(v)))
scales
    major: C   D   E   F   G   A   B
    minor: C   D   E♭  F   G   A♭  B♭
    melodic_minor: C   D   E♭  F   G   A   B
    harmonic_minor: C   D   E♭  F   G   A♭  B
    major_blues: C   D   E♭  E   G   A
    minor_blues: C   E♭  F   G♭  G   B♭
    pentatonic_major: C   D   E   G   A
    pentatonic_minor: C   E♭  F   G   B♭
    pentatonic_blues: C   E♭  F   G♭  G   B♭
chords
    major: C   E   G
    major_6: C   E   G   A
    major_6_9: C   E   G   A   D
    major_7: C   E   G   B
    major_9: C   E   G   B   D
    major_13: C   E   G   B   D   F   A
    major_7_#11: C   E   G   B   F♯
    minor: C   E♭  G
    minor_6: C   E♭  G   A
    minor_6_9: C   E♭  G   A   D
    minor_7: C   E♭  G   B♭
    minor_9: C   E♭  G   B♭  D
    minor_11: C   E♭  G   B♭  D   F
    minor_7_b5: C   E♭  G♭  B♭
    dominant_7: C   E   G   B♭
    dominant_9: C   E   G   B♭  D
    dominant_11: C   E   G   B♭  D   F
    dominant_13: C   E   G   B♭  D   F   A
    dominant_7_#11: C   E   G   B♭  F♯
    diminished: C   E♭  G♭
    diminished_7: C   E♭  G♭  B♭♭
    diminished_7_half: C   E♭  G♭  B♭
    augmented: C   E   G♯
    sus2: C   D   G
    sus4: C   F   G
    7sus2: C   D   G   B♭
    7sus4: C   F   G   B♭
模式
模式本质上是刻度的左旋。
mode = rotate
需要注意的是,由于旋转后的根音会发生变化,因此所得到的旋转比例或模式处于不同的键中。
对于每个键,主要有七个主要音阶模式,具体取决于所应用的左旋次数,每个模式都有一个特定的名称:
major_mode_rotations = {
    'Ionian':     0,
    'Dorian':     1,
    'Phrygian':   2,
    'Lydian':     3,
    'Mixolydian'4,
    'Aeolian':    5,
    'Locrian':    6,
}
使用此方法,我们现在可以为任何给定键生成主要比例的模式。这是C大调的一个例子:
intervs = make_intervals('C''major')
c_major_scale = make_formula(formulas['scales']['major'], intervs)
for m in major_mode_rotations:
    v = mode(c_major_scale, major_mode_rotations[m])
    print('{} {}: {}'.format(dump([v[0]]), m, dump(v)))
这就是结果。请记住,根音随着每次旋转而变化:
C   Ionian: C   D   E   F   G   A   B
D   Dorian: D   E   F   G   A   B   C
E   Phrygian: E   F   G   A   B   C   D
F   Lydian: F   G   A   B   C   D   E
G   Mixolydian: G   A   B   C   D   E   F
A   Aeolian: A   B   C   D   E   F   G
B   Locrian: B   C   D   E   F   G   A
上面,我们正在研究从给定比例导出的模式。但是,实际上我们关心的是给定键的模式。因此,给定C的键,我们想知道C IonianC DorianC Mixolydian等。
另一种表达方式是,例如“ C Mixolidian”“the Mixolydian of C”不同。前者是指根音为C的混合音阶,后者是指C大音阶的混合音阶(即上方的G混合音阶)。
我们还可以非常轻松地在给定键中生成模式。
keys = [
    'B#',  'C''C#''Db''D''D#',  'Eb''E',  'Fb''E#',  'F',
    'F#''Gb''G''G#',  'Ab''A''A#',  'Bb''B',  'Cb',
]

modes = {}
for key in keys:
    intervs = make_intervals(key, 'major')
    c_major_scale = make_formula(formulas['scales']['major'], intervs)
    for m in major_mode_rotations:
        v = mode(c_major_scale, major_mode_rotations[m])
        if v[0not in modes:
            modes[v[0]] = {}
        modes[v[0]][m] = v
上面,我们循环了每个键,并建立了一个字典,其中包含我们遇到每个键时所使用的模式(通过检查模式的第一个音符)。
现在,例如,如果我们打印出模式['C'],则会得到以下内容:
总结
因此,我们研究了西方音乐理论中的基本音符。如何从这些音节中得出音阶。如何利用间隔名称从谐音等效项中选择正确的音符。然后,我们研究了如何使用间隔公式(使用标准音程名称和相对于大音阶的音程)来生成各种音阶和和弦。最后,我们看到模式只是音阶的旋转,对于给定的键可以用两种方式查看:通过旋转给定键的音阶(将在另一个键中)得出的模式,以及从模式中得出的模式。从某些键开始,这样第一个音符就是我们想要的键。

更多阅读



5 分钟快速上手 pytest 测试框架


5分钟掌握 Python 随机爬山算法


5分钟快速掌握 Adam 优化算法

特别推荐




点击下方阅读原文加入社区会员

浏览 71
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报