08–06–2022

Так что в этом году я поехал на свой первый после пандемии фестиваль — хакерский лагерь Электромагнитное поле 2022 (или emfcamp, как его многие называют). Свой вклад в проведение фестиваля внесла демо-вечеринка FieldFX. Я решил, что хочу принять участие в демоверсии, основанной на моей работе ZX Spectrum. Я подумал, что воспользуюсь этой возможностью, чтобы немного больше рассказать о программировании ZX Spectrum, немного углубившись в графику и звук.

Если вам не терпится, вы можете посмотреть демонстрацию в видео выше. В качестве альтернативы вы можете скачать файл крана, просмотреть запись на pouet или взглянуть на demozoo.

Графика Speccy

ZX Spectrum 48K имеет эффективный размер экрана 256 × 192 пикселей. У нас есть выбор из 16 цветов, 8 обычных и 8 ярких версий обычных цветов, включая всеми любимый ярко-черный. В Википедии есть довольно хорошая статья о различных цветах и ​​графических возможностях Speccy, и ее стоит прочитать.

Рисовать на ZX Spectrum — дело непростое. Spectrum был в некоторой степени разработан для текста, поэтому расположение экранной памяти было построено для облегчения этого. Графическая память начинается с адреса 0x4000, и вы думаете, что она будет линейной? К сожалению, это не так. Вы можете прочитать отличное описание того, как все это работает — я позаимствовал видео и код и воспроизвел их здесь. Это легче увидеть, чем объяснить.

Если мы начнем с этой BASIC-программы, заполняя видеопамять линейным образом, мы получим следующее видео:

10 for i = 0 to 6144
20 poke 16384 + i, 255
30 next i

https://archive.section9.co.uk/screen-lines.mp4

У нас есть три банка памяти. Экран заполняется каждым банком в порядке сверху вниз. Каждый банк заполняется путем заполнения строки пикселей каждой строки символов. Как только первая строка каждой строки символов заполнена, мы переходим ко второй и так далее. Каждая символьная строка имеет длину в байт, где каждый бит соответствует одному пикселю.

Каковы последствия этого? Во-первых, мы не рисуем отдельные пиксели. Мы рисуем 8 пикселей за раз в строке, используя один байт. Это приводит к тому, что у нас есть классический вид ZX Spectrum столкновение цветов. Когда мы устанавливаем цвет, мы делаем это либо для каждой строки пикселей (8 x 1), либо, чаще, для каждого символа (8 x 8). Второй вывод заключается в том, что нам нужен какой-то странный код, чтобы выяснить, где находится следующая позиция памяти, если мы хотим обращаться к экрану, используя наши стандартные координаты X, Y.

К счастью, у известного демосценера и музыканта Gasman есть репозиторий на github с удобным набором полезных процедур, в том числе той, которая определяет, где будет следующая позиция Y в памяти для рисования. Я воспроизвел это здесь:

; Given an address in screen memory in DE, return the address of the next pixel line down
upde:
        inc d
        ld a,d
        and 7
        ret nz
        ld a,e
        add a,32
        ld e,a
        ret c
        ld a,d
        sub 8
        ld d,a
        ret

Я не буду вдаваться в подробности. Достаточно сказать, что если мы поместим 0x4000 в пару регистров DE, а затем вызовем эту функцию, DE будет установлен на следующую строку вниз.

Я знаю, это звучит немного безумно, но у нас есть начало демо прямо здесь!

Как? Что ж, теперь можно рисовать линию байт за байтом, находить следующую строку и рисовать ее байт за байтом, пока не закончим. Мы можем рисовать одноцветные растровые изображения на экране. Этого должно быть достаточно, чтобы мы начали. Я написал скрипт на Python, который конвертирует простые изображения в блок байтов (он находится в моем github-репозитории). Наконец, нам нужна какая-то процедура рисования, которая просматривает блок памяти и рисует этот блок на экране. Это тоже есть в github repos, но воспроизведу и здесь:

image_width:
    defb 0
image_height:
    defb 0
image_x:
    defb 0
image_y:
    defb 0
image_offx:
    defb 0
image_offy:
    defb 0
draw_set_pos:
    ; Call this first to set DE to the correct drawing position.
    ; TODO could be a bug here if offx is 0 to begin with
    ld a, (image_offx)
loop_offx:
    inc de
    sub 1
    cp 0
    jr nz, loop_offx
    ld a, (image_offy)
    ; Now loop through the y offset
    ; upde uses the accumulator so we must be a bit more clever with offy loop
    ; TODO we always go one line down first. Naughty but easier :/
loop_offy:
    push af
    call upde
    pop af
    sub 1
    cp 0
    jr nz, loop_offy
    ret
draw_bitmap:
    ; Now we have our final start position in de so push it
    push de
loop_draw_bitmap:
    ; Now draw the next block of 8 pixels
    ld a, (bc)
    ld (de), a
    inc bc
    inc de
    ; read the x pos and subtract. Call next line if needed
    ld a, (image_x)
    sub 1
    cp 0
    jr z, next_line
    ; write the xpos back to memory
    ld (image_x), a
    jr loop_draw_bitmap
next_line:
    ; take the saved width and reset the x counter
    pop de
    ld a, (image_width)
    ld (image_x), a
    ; Now check that y isn't 0
    ld a, (image_y)
    sub 1
    cp 0
    ret z
    ; Write new Y-pos back to memory
    ld (image_y), a
    ; Find the next line down
    push bc
    call upde
    pop bc
    push de
    jr loop_draw_bitmap

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

Затем мы устанавливаем начальную позицию, где мы хотим рисовать. Это части функции draw_set_pos, loop_offx и loop_offy. Нам нужно найти в памяти начальное местоположение того места, где мы хотим нарисовать наше изображение, начиная с 0x4000 и продвигаясь вперед. Начало нашего изображения находится в левом верхнем углу, поэтому image_offx и image_offy — это расстояния от левого верхнего угла экрана Speccy до левого верхнего угла нашего изображения. Обратите внимание, мы вызываем функцию upde, которую определили ранее.

Наконец, мы начинаем рисовать в разделе draw_bitmap. Он состоит из пары циклов, основанных на ширине и высоте изображения, которые мы установили в начале. Берем байт оттуда, где хранится наше изображение, и копируем его в память экрана. Мы перемещаем по одному байту в память экрана и изображения и продолжаем, вычитая один из нашего счетчика image_x. Когда image_x равен нулю, мы вызываем upde, уменьшаем значение счетчика image_y и возвращаем image_x исходное значение и продолжать. Когда image_y равен нулю, мы знаем, что закончили рисование.

Я ожидаю, что это не самый эффективный метод рисования, но я разработал его с небольшой помощью, поэтому я им доволен. Мы можем рисовать простые одноцветные изображения, как показано ниже:

Демонстрация широко использует эту функцию. Это в значительной степени основа всего этого. Следующим логическим шагом, я думаю, было бы сделать это быстрее, с возможностью использовать цвета.

Звук

Speccy 48K не имеет звукового чипа, только небольшой бипер. Тем не менее, можно создать потрясающую музыку. Просто послушайте это:

Сомневаюсь, что мне когда-нибудь будет так хорошо! Тем не менее, мы можем многое сделать с помощью простой звуковой процедуры, которая существует в ПЗУ Speccy. Напомним, что ПЗУ находится в ячейке памяти от 0x0000 до 0x3FFF. Следующий код делает вызов в ПЗУ и издает короткий звуковой сигнал:

basic:
ld hl,noteC1        ; pitch.
ld de,noteC1DH      ; duration.
call 949            ; ROM beeper routine.
ret

Мы загружаем hl высотой тона для ноты C, октава 1, и устанавливаем de длительностью этой ноты. Включая и выключая звуковой сигнал очень быстро, мы можем генерировать нужный нам звук. Например, частота ноты ля составляет ровно 440 герц. Если мы будем включать и выключать звуковой сигнал так быстро, мы будем генерировать A. Эту процедуру звукового сигнала я нашел онлайн на chuntey.wordpress.com. Автор довольно подробно описывает, как это работает, но все, что нам нужно знать, это то, какие регистровые пары de и hl должны быть установлены для того, чтобы создать заметку, которую мы хотим.

Формула, приведенная в этом сообщении в блоге, выглядит следующим образом:

DE = Duration = Frequency * Seconds
HL = Pitch = 437500 / Frequency – 30.125

При этом мы можем проработать столько октав, сколько захотим. Напомним, что в октаве 12 нот. Нота в более высокой октаве имеет вдвое большую частоту, чем ее более низкая родственница. Для демонстрации мне понадобилось около 3 октав. Каждой из этих трех октав требовались четвертные и половинные вариации нот (восьмая и четверть секунды соответственно) — тьфу!

В демо есть пара эффектов, которые вы, возможно, заметили — у нас есть пара питчбендов и пара шумовых эффектов. Оба я скопировал с chuntey.wordpress.com, и они работают очень хорошо.

Все звуковые процедуры можно найти в моем репозитории speccy на github с дополнительными пояснениями по их использованию.

Вы бы не щелкнули правой кнопкой мыши по NFT

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

Я решил использовать классическое публичное объявление Ты бы не угнал машину начала 2000-х. Этот короткометражный фильм так много пародировали, что теперь он стал мемом — самый известный из них с фразой Вы бы не стали загружать машину. Все это, по сути, саркастично — вы абсолютно загрузите машину.

Я решил, что хочу взяться за всю эту штуку с NFT, и идея о том, что щелчок правой кнопкой мыши по скучающей обезьяне и сохранение ее на диске — это то же самое, что и кража (очевидно, это явно нет). Весь бизнес NFT созрел для хорошей критики, поэтому воссоздание рекламы на Spectrum было бы особенно забавным — машина, совершенно неспособная иметь дело с NFT, но идеально подходящая для взятия микки из них.

Моя любимая жена — музыкант, поэтому я привлек ее к изучению музыки. Мы записали черновую версию музыки с некоторыми основными нотами. Затем я поместил их в milkytracker (трекер, который я использовал ранее в Демонстрации FPGA), чтобы убедиться, что он звучит правильно.

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

Теперь мы знаем, как рисовать на экране, поэтому нам просто нужно рисовать нужные нам изображения. В рекламе используется шрифт XBand Rough, которого достаточно для большей части демонстрации.

Каждая сцена представляет собой отдельный файл asm, чтобы все было управляемо. Различные подпрограммы хранятся в файлах asm библиотеки, как и отдельные изображения, экспортируемые в формат памяти спектра нашим скриптом python. Со всем этим у нас есть готовая демонстрация!

Реальное оборудование

Для разработки ретро-демонстрации обычно используются современные средства разработки, такие как хороший текстовый редактор, кросс-компиляция и эмулятор. Я уверен, что некоторые люди все еще разрабатывают свои демоверсии на реальной машине, но я подозреваю, что большинство использует современные машины. Тем не менее, я не думаю, что это настоящая демонстрация, пока она не будет протестирована на реальной машине. К счастью, у меня есть ZX Spectrum 48K с DivMMC, полностью готовый к работе, так что, конечно же, мне нужно было увидеть мою демонстрацию, работающую на реальном оборудовании:

Как мы сделали?

Поле-FX было очень весело! Я прекрасно провел время, увидев всех новых людей и демо, которые они сделали. Из 4 записей я был очень близок ко второму. Победитель — Зеленая машина — действительно произвел сильное впечатление. Тем не менее, моя демонстрация вызвала больше всего смеха и аплодисментов вечера — похоже, людям она действительно понравилась! В то время как моя демонстрация не является самой технической ни в коем случае, она находится прямо на пульсе, с точки зрения сообщения. Как я сказал в конце своего последнего сообщения в блоге о демонстрациях, знайте свою аудиторию.

Потребовалось некоторое время, чтобы собрать эту демонстрацию. Это было сделано на моем ноутбуке, когда я сидел на диване в этих фрагментах свободного времени, которое вы получаете вечером. Это очень отличалось от других моих проектов, где я работаю в офисе несколько часов или в мастерской, почесывая бороду за чашкой чая. Медленно откалываюсь в течение нескольких месяцев, учусь на ходу. Это было довольно спокойно и с низким уровнем стресса.

Я бы сказал, что есть много способов улучшить эту демонстрацию. Прежде всего, какой-то эффект шума или масштабирования текста. Он действительно должен всплывать и немного подпрыгивать, прежде чем остановится. Я думаю, мне нужно улучшить процедуру рисования растровых изображений.

Следующая демо-вечеринка в Великобритании будет NOVA, с 12 по 14 августа.