Генератор музыки на базе кодогенератора

Привет, хабраюзер! В этом топике я расскажу о своей идее генерации музыкальных композиций. Создадим язык описания ритма музыки на базе 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 (из-за костылей принимает только это имя).

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

© Habrahabr.ru