Пишем конвертер для генератора мелодий от Nokia 3310

Любителям всего старого, но безумно интересного, добрый вечер! bb3c0c4e0b8c46f08a405b5d20c67334.jpg Помните такой телефон — Nokia 3310? Разумеется, помните! А такую штуку как синтезатор мелодий в нем? Тоже помните, отлично. А по старым, теплым и ламповым мелодиям скучаете? Вот и я скучаю. А еще мне на глаза попался сайтик с более чем сотней нотных листов для этого редактора. И что я должен был оставить эту прелесть без внимания? Нет уж. Что я сделал? Правильно! Взял и написал точно такой же генератор мелодий, который позволяет на выходе получить Wave — файл с мелодией. Интересно, что из этого получилось? Тогда прошу под кат.

Nokia Composer был встроен в целую кучу телефонов, подобных Nokia 3310. Кроме 7 нот, он позволял записать 5 диезов, указать октаву и длительность в частях. А еще были ноты, которые не звучали — паузы. То есть «нота» в Composer’e была действительно нотой.

Сама запись ноты для Composer’a выглядела так:

ec5503e589ae4cd49de195bcded658d1.png

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

Ладно, наговорились.

Давайте напишем скрипт, который будет принимать ноту, как она есть и возвращать кортеж параметров. (пишем на Python 2.7, да) def Parse_Tone (Note): Note = Note.upper () if Note.find (»-») == -1: try: (Duration, Octave) = re.findall (r»[0–9]+», Note) except: pass else: Duration = re.findall (r»[0–9]+», Note)[0] Octave = 1 Tone = re.findall (r»[A-Z,#,-]+», Note)[0] Duration = int (Duration) Octave = int (Octave) if Note.find (».») != -1: Duration = Duration/1.5 return (32/Duration, Tone, Octave) Во! То есть, сначала мы переводим ее в ВЕРХНИЙ РЕГИСТР, а затем — с помощью регулярных выражений разбираем на составляющие. Отдельно проверяем наличие точки (увеличиваем в 1.5 раза) и учитываем паузу.

Готото! Теперь если передать функции, например, 16C2, на выходе получим (2, C, 2) то есть длительность в долях, ноту и октаву.

Что? Откуда взялось число 32? Это простоОригинальный Nokia Composer позволял установить длительность ноты как 1/32 «полной» ноты. При этом для него существуют еще и 1/16, 1/8, 1 / 4, 1 /2 и 1 длительности. То есть каждая следующая длительность отличается от предыдущей ровно в 2 раза. Тогда мы можем сделать вот что:

Возьмем 1/32 ноты как «единичную ноту». Тогда 1/16 — это уже 2 единичных ноты, 1/8 — 4 и так далее. Тогда мы можем взять и поделить 32 на полученную длительность.

С этим разобрались. Теперь осталось понять, как мы будем все это дело превращать в Wav — файл.

Если очень грубо — в Wave файле, кроме заголовка записаны напряжения, которые подаются на динамик. Если чуть точнее — части напряжений от максимального. То есть, если в двухбайтовом фрейме записано число 32765 — это означает, что нужно подать максимальное напряжение. Изменяя уровни напряжений с течением времени, мы можем добиться колебаний мембраны динамика. А если эти колебания будут в нужном нам диапазоне… Правильно! Мы услышим звук определенной частоты.

Теперь, о том, как это сделать.

Давайте напряжем память и… вспомним школьный курс физики! Примерно ту часть, в которой говорится о гармонических колебаниях.

Если очень просто: гармонические колебания — тип колебаний, колеблющаяся величина которых изменяется по закону синуса (ну или косинуса, как хотите)

916a1582ea344e688ff431a7aa4a8b99.jpg

Общая формула этого безобразия выглядит как:

e41a32a11c8743d4bf39db87bb089b4d.png

При этом циклическая частота это

81aef7def5c14b1e818428d0caafcb9d.png

Вспомнили? Отлично! Теперь надо понять — зачем.

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

Result = (32765*VOL*math.sin (6.28*FREQ*i/44100))

Откуда все это взялось? Рассказываю.

32765 — Фрейм у нас двухбайтовый, поэтому максимальное значение амплитуды ровно 32765. VOL — переменная, задающая громкость. Изменяется в диапазоне от 0 (полная тишина) до 1 (орет как на площади)

6.28 — это всего-навсего 2*Pi. Можно каждый раз высчитывать, но мы ж не звери.

FREQ — А это то, ради чего все и затевалось — нужная нам частота.

i/44100 — время, относительно начала отсчета. Почему мы делим на 44100? А потому что это частота дискретизации выходного файла (ну это я так придумал. Можно и меньше. Качество будет ниже). За секунду проходит 44100 отсчетов, поэтому и делим. Надеюсь, получилось объяснить

Ну вот. Один фрейм мы задавать научились. Теперь нужно сделать так, чтобы это все работало. То есть, помимо частоты задать еще и длительность.

А раз уж частота фиксированная… Ага! Обернем в цикл.

Вот в такой.

for i in range (0, TIME/10×441): Result = (32765*VOL*math.sin (6.28*FREQ*i/44100)) Frames.append (Result) Опять непонятности. Откуда взялось TIME/10×441? Из моего воображения. Нет, серьезно. Это я так решил, что минимальное время звучания — 0.001 секунда. Как я уже говорил — один отсчет (при данной частоте дискретизации) это 1/44100 секунды. Соответственно, 0.001 секунда это 44.1 отсчета. А 44.1 = 441/10. А если надо задать N миллисекунд… домножим, ага. Вот мы и получаем то, что написали (TIME — это как раз таки время в миллисекундах, да)

Так ну и обернем все это дело функцию, надеюсь никто не против?

def Append_Freq (VOL, FREQ, TIME): for i in range (0, TIME/10×441): Result = (32765*VOL*math.sin (6.28*FREQ*i/44100)) Frames.append (Result) Во! Теперь мы можем генерировать звук абсолютно любой частоты.

Осталось записать то, что получилось в wave — файл.

Для работы с Wave в Python (по крайней мере в 2.7) есть прелестный модуль с незабываемым названием — Wave. А для работы со всяческими структурами — struct (вообще, до определенного момента, Python — безумно логичный язык).

После некоторых плясок с бубном и прочих извращений получилась вот такая функция:

def Write_Wave (Name): File = wave.open (Name, 'w') File.setparams ((1, 2, 44100, 0, 'NONE', 'not compressed')) Result = [] for frame in Frames: Result.append (pack ('h', frame)) for Each in Result: File.writeframes (Each) (про нее рассказывать не буду, потому как во — первых все понятно, а во — вторых — не будем отдаляться от темы)

Ну вот. Теперь можно сгенерировать звук! Пробуем.

Frames = [] Append_Freq (1, 4000, 5000) Write_Wave ('Sound.wave') Полная громкость, 4 килогерца, 5 секунд.Посмотрим что получилось?

Вот так это звучит:

5000Hz.wav

А вот так выглядит:

bc0f2ae2101741e09af895b2b50bed16.png

Ну, в общем — то, что хотели, то и получили. Звук, правда довольно неприятный.

Кстати, если мне не изменяет память, что в старой библиотеки для Turbo Pascal звук задавался не синусоидой, а меандром. На самом деле достаточно просто изменять напряжение на динамике. Просто синусоида симпатичнее, чем меандр или пила.

Ну вот. Теперь у нас есть функция генерирующая звук нужной частоты и длительности и функция, записывающая то, что мы наделали в настоящий файл.

Теперь нужно научиться записывать ноты.

Чистая (инструментально не окрашенная) нота — это звук определенной частоты.

Диез чистой ноты — звук, с частотой на полтона выше чистой ноты

Бе — моль — звук с частотой на полтона ниже чистой ноты. Бе — моли оригинальный Composer (еще помните, что мы там хотели написать? Отлично!) задавать не дает, поэтому с бе — молями работать не будем. Ну их.

Октава — если упрощенно, это множитель частоты ноты. То есть частота Ре второй октавы вдвое выше той же Ре первой октавы.

Найдем на просторах интернета таблицу нот и их частот

a20293122f434837a74ee726502e7b6b.png

И сделаем из нее словарь.

Вот такой:

Notes = {»-» : 0, «C» : 261.626,»#C» : 277.183, «D» : 293.665,»#D» : 311.127, «E»: 329.628,»#E» : 349.228, «F» : 349.228,»#F» : 369.994, «G» : 391.995,»#G» : 415.305, «A» : 440.000,»#A» : 466.164, «B» : 493.883,»#B» : 523.251} (Вообще, наверно, правильнее писать C#, а не #C, но как правило все мелодии для Composer’a указывались именно в таком формате)

А теперь напишем еще одну функцию, генерирующую звук определенной ноты

def Append_Note (VOL, TIME, NOTE, OCTAVE): for i in range (0, int (TIME/10.0×441)): FREQ = Notes[NOTE]*OCTAVE Result = (32765*VOL*math.sin (6.28*FREQ*i/44100)) Frames.append (Result) #making clear sound if (abs (math.sin (6.28*FREQ*i/44100))>0.01): while (abs (math.sin (6.28*FREQ*i/44100))>0.01): Result = (32765*VOL*math.sin (6.28*FREQ*i/44100)) Frames.append (Result) i+=1 Так, тут надо еще кое — что дорассказать.

С первой частью все понятно — значение нужной частоты берется из словаря, домножается на октаву и пишется в список.

Зачем нужна вторая?

Очень просто. Если желаемая длительность не кратна периоду синусоиды, то в момент времени T1 на динамик может подаваться большое напряжение, а в T1+1 уже ничего подаваться не будет. На мой медвежий слух, это звучит как внезапно оборвавшаяся фраза убитого товарища — неприятно. Поэтому мы доводим нашу синусоиду до ближайшего нуля. При высокой частоте дискретизации заметно это будет мало, а на слух будет выглядеть как та же обрывающаяся фраза товарища, если на глазах мертвеющий (но вопящий) товарищ падает в колодец. Тоже не Бог весть что, но для генерации Нокиевских мелодий сгодится.

Теперь осталось написать функцию, которая будет принимать список нот и поэлементно скармливать его генератору.

def Append_Notes (VOL, LIST, BPM): for Each in LIST: (Duration, Tone, Octave) = Parse_Tone (Each) try: Append_Note (VOL, int (Duration*1000×7.5/BPM), Tone, Octave) except: print «Ошибка! Не могу обработать %s» %Each Append_Note (0, int (250×7.5/BPM),»-», 1) Приблизительно вот так. Снова что — то непонятно? Это нормально, я тоже ничего не понимаю, сейчас разберемся.

BPM — это количество ударов в минуту. Грубо говоря, это «скорость игры». Это самое BPM равно количеству четвертных нот за одну минуту. То есть одна четвертная нота должна играться 60/BPM секунд. А поскольку, мы решили, что длительность единичной ноты у нас это 1/32 — это значение равно 60/32×4/BPM = 7.5/BPM. Звучит одна четвертная нота ровно 1000 миллисекунд (композиторы почему — то так придумали), а потом этот результат домножается еще и на количество таких 1/32 нот.

Когда функция отработает в списке Frame окажется готовый файл, который останется только записать.

Ну и поскольку мне лень писать GUI я люблю консольные интерфейсы, напишем обработчик последовательности нот, который принимает эту последовательность, BPM и имя выходного файла в списке аргументов и скармливает функции Append_Notes ()

def MakeTune ():

if (len (Arguments)!=3): print 'ERROR!\n USAGE:\n Composer «Notes» BMP FileName\nExample:\n Composer »16c2 16#a1 4c2 2f1 16#c2 16c2 8#c2 8c2 2#a1 16#c2 16c2 4#c2 2f1 16#a1 16#g1 8#a1 8#g1 8g1 8#a1 2#g1 16g1 16#g1 2#a1 16#g1 16#a1 8c2 8#a1 8#g1 8g1 4f1 4#c2 1c2 16c2 16#c2 16c2 16#a1 1c2» 120 Music.wave' return 1

List = Arguments[0].split (' ')

BPM = int (Arguments[1])

OutFile = Arguments[2] print »\nFile information:\n\n Note number: %s\n Tempo: %s BPM\n\nGeneration of amplitude…» % (len (List), BPM)

Append_Notes (1, List, BPM) print »\nOk!\n\nWriting Wave File…» Write_Wave (OutFile) File = open (OutFile,'rb').read () Size = len (File) print »\n File size: %.2f MB\n Duration: %.2f c. \n\nAll Done.» % (Size/1024.0/1024, Size/44100/2) Вот и все.

Теперь осталось только передать программе исходные данные и забрать готовую мелодию.

Попробуем?

Ноты 16c2 16#a1 4c2 2f1 16#c2 16c2 8#c2 8c2 2#a1 16#c2 16c2 4#c2 2f1 16#a1 16#g1 8#a1 8#g1 8g1 8#a1 2#g1 16g1 16#g1 2#a1 16#g1 16#a1 8c2 8#a1 8#g1 8g1 4f1 4#c2 1c2 16c2 16#c2 16c2 16#a1 1c2

Вгоняем в генератор…

8694a1c9a35546d5ac7091dc0835794b.png

И забираем результат:

output.wav

По — моему неплохо.

Еще примеров? Легко!

Гимн СССРПод небом голубымОсеньРождественская мелодия (из оригинального 3310)

Хотите сами писать? Попробуйте!

Вот ноты 4d1 4g1 8g1 8a1 8g1 8#f1 4e1 4c1 4e1 4a1 8a1 8b1 8a1 8g1 4#f1 4d1 4d1 4b1 8b1 8c2 8b1 8a1 4g1 4e1 8d1 8d1 4e1 4a1 4#f1 2g1

Вот темп: 200

Пропустите через генератор и посмотрите что получится (А кто-то может и на глаз узнает).

Скрипт генератора

Надеюсь, вам понравилось!

Искренне Ваш, слушающий монофонического Моцарта, GrakovNe

© Habrahabr.ru