musicpy数学音乐:生成协和音程的分形音乐
__阿岳__
编辑于 2023年09月17日 17:13
收录于文集
共19篇

最近发现musicpy做了很多很多的更新,以前有个视频看上去都过时了。

最近看经济周期性危机相关的内容时突然联想到了之前看过的一个分形音乐的视频。(脑回路总是能产生各种奇怪的联想)

这是以前看到的分形音乐的视频:分形音乐​

分形音乐的midi

上图中红色框部分都是一样的,只不过是长度按照倍率压缩了。

这些分形音乐还不是随手写一个小节,然后复制复制复制,拉长拉长就可以生成出来的。

因为最终的结果很容易出现“不协和音程”。例如小二度、减五度、增四度、大七度。

衡量音程协和与不协和与否就是看频率高的音符是频率低的音符的多少倍(频率多少倍),

这个倍数是含有开根号的分数,虽然是无理数但却非常接近一个分数。可以近似看成有理数,如果这个分数化简后分子和分母的数字尽可能的小,那么听起来就是协和的。否则不协和。

这里懒得解释了。因为3b1b有一个视频详细的阐述了这个问题。

音乐与测度论有什么关系?(中英字幕)​这里放一个视频链接。

本文章用Python的musicpy库实现了生成分形音乐mid文件的代码。

运行以下代码需要安装musicpy第三方库,以及新建一个“out”文件夹。

这篇文章写于2023年9月17日,由于musicpy的作者一直在更新,所以这篇文章的代码也可能具有版本局限性。

代码:

代码块
Python
自动换行
复制代码
"""
分形音乐制作

只要一运行这个脚本,脚本就开始源源不断的在当前目录下的out文件夹里生成
协和的分形音乐midi
"""
from random import choice

from musicpy.musicpy import play
from musicpy.structures import note, piece, chord, track


def is_distance_okay(d: int):
    # 音程是否协和
    return d % 12 not in [
        # 非常不协和
        1, 6, 11,
        # 有点不协和
        2,
    ]


def compute_node(node_low, node_high):
    """返回两个音符组成在一起是否协和"""
    return is_distance_okay(abs(node_low.degree - node_high.degree))


def main():
    string = 'CDEFGAB'
    # 每次随机一个音符,十六分,八分,四分
    durations = [1, 2, 4]

    index = 0
    while True:
        index += 1
        s = set()
        cur_len = 0  # 当前已经有的长度,如果超过了16则重新抽取
        note_list = []  # 存放一个小节里的所有音符,note对象
        note_list_16 = []  # 存放一个小节里所有的音符,16分音符离散化,note对象
        dur_list = []  # 存放每个音符对应的时间长度:  ?/16  [?, ?, ?....]

        while cur_len != 16:

            duration = choice(durations)
            if cur_len + duration > 16:
                continue

            cur_len += duration
            dur_list.append(duration)
            n = choice(string)
            s.add(n)

            note_list.append(note(n, 6))
            # 将这个音符按照长度写入这个音轨
            for _ in range(duration):
                note_list_16.append(note(n, 6))

        if is_consonance(note_list_16):
            print('协和', note_list_16, dur_list, note_list, '音符种类数量:', len(s), get_name(s))
            # 将这个测试结果生成mid文件
            gen_midi(note_list, dur_list, f'out/test{index}-{get_name(s)}.mid')


def get_name(s: set) -> str:
    res = []
    for char in s:
        res.append(char)
    res.sort()
    return ''.join(res)


def gen_midi(note_arr, dur_arr, file_name):
    """根据一个音符数组和一个时长数组生成对应的分形音乐midi文件"""

    # midi音色选择,不同音轨 i: 音色编号
    # https://www1.gdjyw.com/music/new/html/7346.html
    color = {
        6: 1,  # 明亮大钢琴
        5: 1,  # 大钢琴
        4: 13,  # 木琴
        3: 1,  # 吉他
        2: 19,  # 教堂管风琴
        1: 49,  # 弦乐合奏,(低音提琴)
    }
    p = piece(
        [],  # 存放的每一个轨
        [color[i] for i in range(1, 7)],  # 每一个轨道的乐器
        120,  # bpm
        [0] * 6,  # 起始时间
        name='demo'
    )
    for i in range(1, 6 + 1):
        # 从低音区到高音区 1 2 3 4 5 6 组号
        scale = 2 ** (6 - i)  # 时间拉长的倍数
        down_scale = 6 - i  # 降低多少个八度
        copy_count = 2 ** (i - 1)  # 自身有多少段

        # 降低八度
        note_list = [note_item.down(12 * down_scale) for note_item in note_arr]
        # 时间拉长
        time_list = list(map(lambda x: (x / 16) * scale, dur_arr))
        # 生成chord段
        c = chord(note_list, time_list, time_list)
        # 重复次数
        c *= copy_count
        # 将chord放入轨道
        tr = track(c, color[i], 0)  # 和弦,乐器,延迟
        p.append(tr)

    play(p, name=file_name)


def is_consonance(arr):
    """
    检验一个分形结构能否生成和谐的分形音乐
    arr = [
        note('C', 6), note('C', 6), note('C', 6), note('C', 6),
        note('G', 6), note('G', 6), note('D', 6), note('D', 6),
        note('E', 6), note('E', 6), note('D', 6), note('D', 6),
        note('E', 6), note('E', 6), note('D', 6), note('D', 6)
    ]
    """
    lists = [
        arr
    ]

    for i in range(5):
        # 拽长数组中的最后一个子数组并添加
        lists.append(long2(lists[-1]))

        # 最后一个音轨降低八度
        for j in range(len(lists[-1])):
            lists[-1][j] = lists[-1][j].down(12)

        # 前面的所有子数组都 ctrl D
        for j in range(len(lists) - 1):
            lists[j] = copy2(lists[j])
    # 检测这个分形音乐是否含有不协和音程
    # 两个相邻八度音轨、两个相邻八度音轨的看
    try:
        for t in range(len(lists[0])):
            for y in range(0, len(lists) - 1):
                n1 = lists[y][t]
                n2 = lists[y + 1][t]
                if not compute_node(n1, n2):
                    raise ValueError("...")
    except ValueError:
        return False
    return True


def long2(arr):
    # 将一个音轨拽长2倍
    arr2 = []
    for item in arr:
        arr2.append(item)
        arr2.append(item)
    return arr2


def copy2(arr):
    # 将一个音轨复制两倍
    return arr + arr


if __name__ == '__main__':
    main()
复制成功

代码写的比较随性,直接创建了一个草稿,写在了一个py文件里。可能有一些命名也比较随意。