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