Привет, хабраюзер! В этом топике я расскажу о своей идее генерации музыкальных композиций. Создадим язык описания ритма музыки на базе python, напишем компилятор этого языка в wave файлы и получим довольно нехилую электронную композицию.
Добро пожаловать под кат.
Кому не терпится послушать музончик сейчас, то вот онлайн: кликабельно (первые пару секунд неудачны, дальше вполне нормально).
Вместо вступления
Прочитал я тут на хабре статьи про генерацию музыки (google.ru > генерация музыки site:habrahabr.ru ), понравилось. А потом наткнулся на трэшгены (генераторы мусорного кода). Все это время я слушал музыку и обратил внимание на то, что в каждой композиции есть повторяющиеся ноты.
Например:
тынц тынц, пам пам, парам пам пам тынц тынц, пам пам, парам пам пам тынц тынц, пам пам, парам пам пам
И возникла у меня идея, чтобы представлять все эти последовательности звуков, как циклы, в теле которых закодированы эти звуки. Это чем-то похоже на алгоритмы «школьного сжатия данных», когда перед повторяющимися данными мы пишем количество их повторений.
Итак, у нас есть задача:
1) Спроектировать язык описания ритма
2) Написать компилятор в байт-код (последовательность звуков)
3) Оцифровать и записать в wave-файл
Приступим.
После долгих изысканий в теории компиляторов, написании лексеров и разбитии на токены мне это дело надоело. Было решено использовать, внимание, синтаксис языка Python. Да-да, именно. Данный язык поддерживает выражения вида yield statement.
Тема yield достаточно обширна и если вы не знакомы с ней и желаете ознакомиться, то я вас посылаю к статье «Как работает yield».
Мы же продолжим. Итак, давайте условимся.
Для представления некоторого звукового сигнала (далее — фрейм) мы будем использовать функцию вида n(diap[0], diap[1]), где n — числовой номер этой функции. Где diap — список или кортеж начального значения и конечного диапазона генерируемых частот.
Использовать для кодирования ее вызовов будем выражение вида:
Чтобы придать ясности вот пример из выхлопа генератора где-то в центре кода:
В данном исходном тексте есть yield «0», означающий, что в данном месте будет нулевая последовательность байт, для придания пауз между фреймами (чтобы музыка не вышла сплошным звуком).
Это означает (из выдранного контекста) следующую последовательность:
Теперь рассмотрим что из себя представляет функция фрейма.
При вызове n(diap[0], diap[1]) в ассоциативный массив добавляется ключ n со случайным значением r, где diap[0] <= r <= diap[1]
Это потребуется для исполнения виртуальной машиной нашего байт кода фреймов.
Итак, настало время компилировать.
Как же мы будем это делать?
Для начала нам надо пройтись по нашему сгенерированному коду и составить словарь в котором ключи — номер функции, а значение — случайная величина из диапазона. Можно это делать при парсинге кода, а можно прямо во время генерации. У меня именно второй вариант.
Наш код описания ритма ( далее КОР ) мы можем представить в виде:
Внимание: в коде используется три раза двойная кавычка "
Теперь мы храним наш код, как строку. В Python есть функция exec, которая позволяет выполнить код. Посмотрим ее применение:
При вызове my_code и передав ей в качестве параметра строку с кодом, мы получим генератор списка, генерирующий последовательность байт-кода, то есть:
В lst будет список последовательных вызовов фрейм-функций нашего КОРа.
То есть, в качестве того же примера,
Мы уже имеем ассоциативный массив ( я генерировал его при кодогенерации ), нам осталось только пройтись по lst, выдрать оттуда номера функций ( ключи для словаря ), получая их значения при помощи обращения к нашему словарю.
Тут идет этот процесс и оцифровка c записью в wave:
Весь код доступен на гитхабе (внимание: в генераторе встречается говнокод, так как я раз 20 переписывал этот код и передергивал синтаксис языка, пока не пришел к идеальному консенсусу. Рефакторинг не проводился).
P.S. Запускать модуль Main.py, сохраняя результат генератора в out.py (из-за костылей принимает только это имя).
Добро пожаловать под кат.
Кому не терпится послушать музончик сейчас, то вот онлайн: кликабельно (первые пару секунд неудачны, дальше вполне нормально).
Вместо вступления
Прочитал я тут на хабре статьи про генерацию музыки (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 (из-за костылей принимает только это имя).
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Поделиться с друзьями
Комментарии (4)
Sergiy
10.11.2016 14:57Скорее у вас яблоко, цвет которого становится известным, как только вы на него посмотрите, а до этого оно не красное и не зеленое.
SolidMinus
01.07.2016 17:50Фактически, да. Произвольные куски байт-кода, только следует заметить, что модуль читает их не произвольно и не последовательно, а итерационно, следуя некоторому ритму, который тоже генерируется случайно. И вот во время чтения происходит конкатенация всего этого в единый трек, да.
questor
Если хотите получить более впечатляющие результаты — то лучше двигаться вот в каком направлении. Основа мелодии — рифф, вам нужно научить генерировать интересный рифф, а потом уже подбирать вступительную часть, коду и т.п. Также где-то на хабре были статьи про то, как пользователи в сети голосовали за мелодии и выбирали таким образом лучшие, на их базе генерировались новые — это тоже стоящее направление.
PS Всё равно по факту нужно учить машину композиторскому ремеслу, чтобы достичь новых вершин. Вы успомянули слово «трэшген» — и у меня возникла вот какая ассоциация: в музыке были группы, которые делали композиции, играя на мусорных баках и кастрюльках — в итоге всё равно их влияние на музыку намного меньше, чем у Баха.