Привет, хабраюзер! В этом топике я расскажу о своей идее генерации музыкальных композиций. Создадим язык описания ритма музыки на базе python, напишем компилятор этого языка в wave файлы и получим довольно нехилую электронную композицию.

Добро пожаловать под кат.

Кому не терпится послушать музончик сейчас, то вот онлайн: кликабельно (первые пару секунд неудачны, дальше вполне нормально).

Вместо вступления

Прочитал я тут на хабре статьи про генерацию музыки (google.ru > генерация музыки site:habrahabr.ru ), понравилось. А потом наткнулся на трэшгены (генераторы мусорного кода). Все это время я слушал музыку и обратил внимание на то, что в каждой композиции есть повторяющиеся ноты.

Например:

тынц тынц, пам пам, парам пам пам тынц тынц, пам пам, парам пам пам тынц тынц, пам пам, парам пам пам

И возникла у меня идея, чтобы представлять все эти последовательности звуков, как циклы, в теле которых закодированы эти звуки. Это чем-то похоже на алгоритмы «школьного сжатия данных», когда перед повторяющимися данными мы пишем количество их повторений.

Итак, у нас есть задача:

1) Спроектировать язык описания ритма
2) Написать компилятор в байт-код (последовательность звуков)
3) Оцифровать и записать в wave-файл

Приступим.

Язык описания ритма


После долгих изысканий в теории компиляторов, написании лексеров и разбитии на токены мне это дело надоело. Было решено использовать, внимание, синтаксис языка Python. Да-да, именно. Данный язык поддерживает выражения вида yield statement.

Тема yield достаточно обширна и если вы не знакомы с ней и желаете ознакомиться, то я вас посылаю к статье «Как работает yield».

Мы же продолжим. Итак, давайте условимся.

Для представления некоторого звукового сигнала (далее — фрейм) мы будем использовать функцию вида n(diap[0], diap[1]), где n — числовой номер этой функции. Где diap — список или кортеж начального значения и конечного диапазона генерируемых частот.

Использовать для кодирования ее вызовов будем выражение вида:

yield "n(diap[0], diap[1])"


Чтобы придать ясности вот пример из выхлопа генератора где-то в центре кода:
               yield "19(400,800)"
                for _ in range(7):
                    yield "0"
                    yield "20(400,800)"
                yield "0"
                yield "21(400,800)"
                yield "22(400,800)"
                yield "0"


В данном исходном тексте есть yield «0», означающий, что в данном месте будет нулевая последовательность байт, для придания пауз между фреймами (чтобы музыка не вышла сплошным звуком).

Это означает (из выдранного контекста) следующую последовательность:

19(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 21(400,800); 22(400,800); 0


Теперь рассмотрим что из себя представляет функция фрейма.

При вызове n(diap[0], diap[1]) в ассоциативный массив добавляется ключ n со случайным значением r, где diap[0] <= r <= diap[1]
Это потребуется для исполнения виртуальной машиной нашего байт кода фреймов.

Компиляция в байт-код фреймов и сборка в .wav


Итак, настало время компилировать.

Как же мы будем это делать?

Для начала нам надо пройтись по нашему сгенерированному коду и составить словарь в котором ключи — номер функции, а значение — случайная величина из диапазона. Можно это делать при парсинге кода, а можно прямо во время генерации. У меня именно второй вариант.

Наш код описания ритма ( далее КОР ) мы можем представить в виде:
code = """
def temp():
  тут наш код
"""

Внимание: в коде используется три раза двойная кавычка "

Теперь мы храним наш код, как строку. В Python есть функция exec, которая позволяет выполнить код. Посмотрим ее применение:

def my_code(cd):
    namespace  = {}
    exec(cd,namespace)
    return namespace["temp"]()


При вызове my_code и передав ей в качестве параметра строку с кодом, мы получим генератор списка, генерирующий последовательность байт-кода, то есть:

    print("Compiling...")
    lst = list(my_code(out.code))
    print("Compiled!")


В lst будет список последовательных вызовов фрейм-функций нашего КОРа.

То есть, в качестве того же примера,

19(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 20(400, 800); 0; 21(400,800); 22(400,800); 0


Мы уже имеем ассоциативный массив ( я генерировал его при кодогенерации ), нам осталось только пройтись по lst, выдрать оттуда номера функций ( ключи для словаря ), получая их значения при помощи обращения к нашему словарю.

Тут идет этот процесс и оцифровка c записью в wave:
Линковка в wave-файл
music = wave.open('out.wav', 'w')
    music.setparams((2, 1, freq, 0, 'NONE', 'not compressed'))

    for i in lst:
        if (i == "0"):
            packed_value = wave.struct.pack('h', 0)
            for _ in range(100):
                music.writeframes(packed_value)
            continue

        key = i[0:i.find("(")]
        frame = Syntax.struc.num[int(key)]

        duration = 0.05
        samplerate = freq  # Hz
        samples = duration * samplerate
        frequency = frame #Hz
        period = samplerate / float(frequency)  # in sample points
        omega = N.pi * 2 / period

        xaxis = N.arange(int(period), dtype=N.float) * omega
        ydata = 16384 * N.sin(xaxis)

        signal = N.resize(ydata, samples) # 2-й параметр - скорость

        ssignal = b''
        for i in range(len(signal)):
            ssignal += wave.struct.pack('h', int(signal[i]))  # transform to binary

        music.writeframes(signal)

    music.close()



Весь код доступен на гитхабе (внимание: в генераторе встречается говнокод, так как я раз 20 переписывал этот код и передергивал синтаксис языка, пока не пришел к идеальному консенсусу. Рефакторинг не проводился).

P.S. Запускать модуль Main.py, сохраняя результат генератора в out.py (из-за костылей принимает только это имя).
Нужно продолжение?

Проголосовал 61 человек. Воздержалось 26 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Поделиться с друзьями
-->

Комментарии (4)


  1. questor
    01.07.2016 10:58
    +1

    Если хотите получить более впечатляющие результаты — то лучше двигаться вот в каком направлении. Основа мелодии — рифф, вам нужно научить генерировать интересный рифф, а потом уже подбирать вступительную часть, коду и т.п. Также где-то на хабре были статьи про то, как пользователи в сети голосовали за мелодии и выбирали таким образом лучшие, на их базе генерировались новые — это тоже стоящее направление.

    PS Всё равно по факту нужно учить машину композиторскому ремеслу, чтобы достичь новых вершин. Вы успомянули слово «трэшген» — и у меня возникла вот какая ассоциация: в музыке были группы, которые делали композиции, играя на мусорных баках и кастрюльках — в итоге всё равно их влияние на музыку намного меньше, чем у Баха.


  1. Sergiy
    10.11.2016 14:57

    Скорее у вас яблоко, цвет которого становится известным, как только вы на него посмотрите, а до этого оно не красное и не зеленое.


    1. SolidMinus
      01.07.2016 17:50

      Фактически, да. Произвольные куски байт-кода, только следует заметить, что модуль читает их не произвольно и не последовательно, а итерационно, следуя некоторому ритму, который тоже генерируется случайно. И вот во время чтения происходит конкатенация всего этого в единый трек, да.


  1. Nakilon
    05.07.2016 13:28

    Ruby, как всегда, впереди: http://sonic-pi.net/