Создание демки специально для HABR — Часть 3
Наконец-то мы подобрались к финальной части большой саги о создании демки для ПЭВМ «Микроша». В предыдущих первой и второй части я реализовал заходник, само видео вращения, и понял, как сделать звук. Теперь осталось всё свести воедино, исправить некоторые недочёты и добавить нормальную музыку. Но, как обычно, дьявол кроется в мелочах.
Подготовка мелодии и конвертация
После того как я получил нужный мне midi-файл и код, стала задача написать конвертер. Немного покурив библиотеку mido
и способы воспроизведения звуков в Linux, я переписал программу man_of_letters так, чтобы можно было прослушать midi-файлы локально. В том виде, в каком они звучали бы и на «Микроше». Скажу сразу, звучание на ПК через этот преобразователь и на «Микроше» действительно мало чем отличается. Эта программа нужна для проверки правильности и подготовки midi-файла, чтобы понять, возможно ли будет его корректно конвертировать в формат для «Микроши».
Для того чтобы вывести звук нужной частоты, я использовал консольное приложение play
. Пример ниже звучит пять секунд, с частотой 200 Гц и громкостью 50%.
play -n synth 5 sine 200 vol 0.5
В результате получился вот такой вот вариант проигрывателя. Код переписал под linux, и есть некоторые отличия от предыдущей версии, но они не очень значительные. Главное — способ воспроизведения изменился.
Адаптированный под линукс проигрыватель midifrom mido import MidiFile from pathlib import Path import time import os def noteToFreq(note): a = 440 #frequency of A (coomon value is 440Hz) return (a / 32) * (2 ** ((note - 9) / 12)) mid = MidiFile('ppk.mid', clip=True) print('number of tracks', len(mid.tracks)) note_time_scale = 1 pause_time_scale = 1 note = {'wait':0, 'freq':0, 'dur': 0 } last_note = None for x in mid.tracks[0]: if x.type == 'note_on': if x.velocity != 0: note['wait'] = x.time note['freq'] = int(noteToFreq(x.note)) if x.velocity == 0: note['dur'] = x.time if note['wait']>4: time.sleep(note['wait'] * pause_time_scale / 1000) else: time.sleep(0.01) note_length = int(note['dur'] * note_time_scale) / 1000 print(f" dur = {note['dur']}") cmd = f"play -n synth {note_length} sine {note['freq']} vol 0.5" os.system(cmd) last_note = note
Теперь осталось подготовить midi-файл для конвертации в «Микрошу». Тот файл, который мне больше всего понравился и отвечал почти всем требованиям, всё же содержит партию левой руки. Можно послушать полную версию оригинала вот тут, либо скачать его и послушать локально.
Чтобы он мог быть воспроизведён на «Микроше», необходимо удалить партию левой руки. Для этой цели я воспользовался программой MuseScore. Если ей открыть midi-файл, то партия левой руки видна сразу.
Задача, добиться того, чтобы не было одновременного звучания двух и более нот, только по одной ноте звука. И, широким жестом, а также с помощью клавиши Del, без зазрения совести удаляю всё лишнее. В результате — получаю такой обрубок.
Удалена вся партия левой руки
После такого варварского обрезания осталось проверить, будет ли она хоть как-то вменяемо звучать и можно ли её после этого слушать. Вы можете это сделать онлайн тут либо скачать и послушать локально. Конечно, обрубок, да, но звучит недурно и основной мотив угадывается. Главное качество конкретно этой мелодии, что она прекрасно циклически стыкуется, и непонятно где начало и конец.
Теперь осталось только конвертировать полученный результат в ассемблеровский файл. У меня была простая идея: вызов функции воспроизведения мелодии запускать в функции задержки. Ибо, это единственный способ с более-менее вменяемыми таймингами делать вывод звука. Никаких других фоновых процессов для вывода звука придумать мне не удалось.
Структура мелодии достаточно простая:
- Два байта — магическое число для записи в регистр, если оно равно нулю — это пауза между нотами.
- Один байт — какое количество функций задержки должна длиться данная нота/пауза (как вы помните, она длится 0,027 с).
Поскольку, всё же, все задержки получены эмпирическим путём, некоторые коэффициенты пришлось тоже подгонять по ходу пьесы. Код конвертера написан именно для этой мелодии. В силу её особенностей, пауза между нотами представляет собой нулевую (не играющую) ноту. Если вы захотите использовать данный код для конвертации других мелодий, то стоит обратить на это внимание и доработать код. Полный код конвертера вы можете посмотреть тут. Приведу только небольшой фрагмент:
delay_const = 0.05 #коэфициент задеркжи, подбирал эмпирически, чтобы звучало
last_note = None
delay_s = 0
fp_melody = open('../include/melody.asm', 'w')
fp_melody.write('melody:\n')
for x in mid.tracks[0]:
if x.type == 'note_on':
if x.velocity != 0:
note['wait'] = x.time
note['freq'] = int(noteToFreq(x.note))
if x.velocity == 0:
note['dur'] = x.time
if note['wait']>4:
delay_s = int(note['wait'] * pause_time_scale / (1000 * delay_const))
else:
delay_s = 1
if delay_s:
fp_melody.write(f" dw 0x0000\n db 0x{delay_s:02x}\n")
delay_s = 0
note_length = int(note['dur'] * note_time_scale / (1000 * delay_const))
coef = int(1770000 / note['freq']) #расчёт магического числа
fp_melody.write(f" dw 0x{coef:04x}\n db 0x{note_length:02x}\n")
last_note = note
Сам код воспроизведения мелодии разбирать не буду, потому что читатели на этом этапе уснут, хотя там, по сути, ничего сложного. Ознакомиться с ним вы можете вот тут. Если вы захотите данный пример использовать для прослушивания конвертированных midi-файлов, то можно просто вначале раскомментировать несколько строк:
; org 0
; call init_sound
;while_true:
; call frame_delay
; jmp while_true
Всё, проигрыватель готов, осталось навести пару штрихов и можно идти в люди.
Финальные штрихи
Дьявол кроется в мелочах и порой такие детали могут составлять чуть ли не половину работы. Так и тут, на эту ерунду я потратил чуть ли не 40% всего времени, которые делал демку.
Мне захотелось сделать так, чтобы между первой сценой приветствия и сценой вращения логотипа была анимация поднимаемой портьеры, как в театре. Когда идёт смена кадра, портьера поднимается, постепенно обнажая логотип, снизу вверх.
Второе желание — это избавиться от междустрочного интервала, который по умолчанию присутствует во всех программах на «Микроше».
Типичное отображение символов, видны междустрочные интервалы
Одной из особенностей настроек микросхемы КР580ВГ75 во всех примерах, предполагалось изменение настроек ПДП и изменение размеров кадра. Мне жутко не хотелось изменять настройки контроллера ПДП, потому что в таком случае — пришлось бы переписывать весь код видеовыхода и переделывать все программы. И моё главное желание состояло в том, чтобы отделаться малой кровью.
В общем, создал файл, в котором попробовал все найденные мной примеры. Вы можете сходить и поглядеть, вариантов больше пяти. Однако, ничего корректно не работало, то какие-то артефакты, то двоение, то лезет всякий мусор и некорректно отображаются символы. Скажу так, ЭЛТ — это та ещё магия.
В результате я психанул и начал искать готовые программы, которые используют этот режим. Поскольку я уже наизусть знал, какие там регистры и как должна выглядеть данная функция, то прекрасно себе это представлял. Именно поэтому я без труда смог бы её отыскать. Для примера взял игру Ball, запустил и увидел, что там как раз и используется такой режим. Ну уже дело техники, открываю данную программу в отладчике и просматриваю все этапы инициализации, шаг за шагом, и в конце концов — нахожу готовый рабочий пример.
Почему множество моих вариантов не работало, к сожалению, я так и не понял, ведь этот код ничем особенно не отличается от них. Но я уже так устал бодаться с этой ерундой, что просто взял это готовое решение, внедрил в свой проект и получил результат, который меня устраивает.
Изображение, без межстрочных интервалов
С портьерой провозился чуть ли не месяц. Мне хотелось, чтобы этот переход представлял собой полную заливку экрана белым цветом, и он поднимался как штора в театре. Мне удалось это реализовать, и в эмуляторе всё чудесно работало (кто бы мог подумать), но на ЭЛТ-мониторе — изображение пропадало, просто чёрный экран. Далее, по мере подъёма шторы вверх, постепенно шло проявление яркости логотипа, а саму портьеру так и не было видно. В целом, этот эффект можно было бы как-то обыграть, но это выглядело скорее убого, чем как какая-то фишка, и при этой анимации — изображение на экране дрожало. Выглядело так ужасно и было совершенно не то, что я задумал.
Потратив неделю на разные опыты, доработку кода, пришёл к тому, что портьера стала не сплошной заливкой, а шашечками (подобрав необходимый символ). Из других доработок — решил выводить изображение портьеры не на полный фрейм 78×30, а как рекомендуется в документации — 64×25. Пускай это выглядит не так эффектно, зато корректно работает.
Портьера
Сам код отображения портьеры стал едва ли не самым сложным во всей программе демки. Как оказалось, отображение фреймов и звука проще, чем алгоритмическая генерация изображения. Любопытствующие могут попытаться разобраться с этой громадной простынёй кода, функции splash_screen.
Аппаратное обеспечение
Мне хотелось, чтобы демка была максимально аутентичной и загружалась по-честному — с кассеты, а воспроизводилась — на отечественном мониторе «Электроника» МС 6105 (ака «Колокольчик»). В связи с этим в момент разработки ПО, я занялся поиском по различным аукционам этого аппаратного обеспечения.
▍ Магнитофон
В те времена каждый отечественный магнитофон имел вход и выход аудио в виде пятиконтактного разъёма. Однако покупать отечественную технику я не торопился, так как опасался, что пассики и механика с тех пор там умерли, а заниматься восстановлением ещё и магнитофона — я не хотел.
Поначалу попробовал взять свой старый двухкассетный магнитофон «Sharp», однако его постигла участь смерти старых пассиков, и кассеты не держали нужный темп. В результате было невозможно корректно записать и воспроизвести программу. Поэтому пришлось отказаться от данной затеи. В результате начал искать подходящее решение на досках объявлений. И, оказалось, что раньше делали специальные магнитофоны, именно для того, чтобы использовать кассеты как носители информации. Сами магнитофоны — вполне обычные, и они вполне могут использоваться по прямому назначению (для проигрывания музыки), и мало отличаются от своих собратьев, но использовать их намного удобнее. Один из таких мне удалось найти на одном барахольном аукционе.
Он имеет очень удобный форм-фактор, и на нём написано прямым текстом: «Computer program recorder». Продавец оказался хохмачом и вложил вместе с магнитофоном кассету «Сектора газа», из-за которой я долго не мог использовать магнитофон, потому что слушал музыку и пускал слёзы ностальгии.
На боку магнитофона имеются три разъёма, один стандартный пятиконтактный и два джека. Мною использовались джеки, так как ранее я уже изготовил переходники для них.
Разъёмы, специально для компьютера
Он прекрасно на микрофонный вход принимает полный линейный сигнал с компьютера, и без проблем пишет, а потом воспроизводит программу.
▍ Монитор
В качестве монитора хотел использовать «Электроника» МС 6105 (ака «Колокольчик») потому, что мне казалось, что он выглядит ну очень классно. И когда видел фотографии у xlat, то прям хотелось иметь его в хозяйстве.
В результате купил целых два монитора, но рабочим оказался только один. После того как отмыл его от вековой грязи, он стал выглядеть следующим образом.
Однако к сожалению, показывает он изображение не также классно, как он выглядит. То ли там умерли конденсаторы, то ли настолько села трубка, но изображение долго рябит, пока он окончательно не прогреется (минут 10). Также есть прям севшие пятна на люминофоре, и, хотя мне в целом нравится, как он показывает, на видео это выглядит не так круто, как на охранном мониторе.
Рябое изображение с тёмными пятнами. Это так он показывает
К сожалению, с мониторами мне не очень повезло, хотя с ними выглядит реально стильно.
Результат
Многие из вас уже устали читать эту громадную простыню текста, хотя я не рассказал даже о 20% проблем, которые у меня были, только так, пробежался по верхам.
Но цель всего этого действа — рабочая демка. Вам её и продемонстрирую.
Она по-честному грузится с кассеты, а артефакты на видео — это особенности работы монитора.
Заключение
Это самый безумный и упоротый проект, который я делал просто так, для души, просто ради хорошей статьи для хабра. Задача, которая испытала меня на прочность: «Могу ли я сделать это, хватит ли у меня сил довести всё до конца?». Самое сложное, после того как кончается вдохновение и силы, найти возможность сделать всё, и сделать это хорошо, иначе зачем это делать вообще.
В этой серии статей мне не удалось рассказать и трети всех возможных проведённых экспериментов, пройденных путей, сложностей, с которыми удалось столкнуться в процессе разработки.
Вначале я обещал, что загрузка будет с кассеты, а воспроизведение — на отечественном мониторе, и я снял и такое видео. Оно получилось не очень красивое, из-за того, что монитор показывает пятнами, а изображение рябое.
В качестве заключения хочу сказать, что девчонки — бегите от ребят, которые пишут демки. Они невероятно упоротые и не замечают ничего, кроме своей задачи :).
Интересно, как много демок было написано просто для Хабра?
Удачи в творчестве!
Полезные ссылки:
- Гитхаб проекта.
- Собранная демка, можно запустить в эмуляторе.
- Intel 8080 Assembly Language Programming Manual.
- Псевдо 3D-демо для Радио-86РК.