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

Привіт, хабраюзер! В цьому топіку я розповім про свою ідею генерації музичних композицій. Створимо мова опису ритму музики на базі 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 points sample
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])) # to transform binary

music.writeframes(signal)

music.close()



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

P. S. Запускати модуль Main.py, зберігаючи результат генератора out.py (з-за милиць приймає тільки це ім'я).

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.