Страница Ивана Рощина > Статьи >

© Иван Рощин, Москва

ZXNet: 500:95/462.53
E-mail: bestview@mtu-net.ru
WWW: http://www.ivr.da.ru

Построение таблицы громкости в плеере Pro Tracker 3

Радиомир. Ваш компьютер» 4/2004)

Музыкальные модули, написанные в редакторе Pro Tracker 3 (далее PT3), проигрываются с помощью специального набора процедур, называемого плеером. В плеере (по крайней мере, для версий 3.3—3.6) имеется так называемая таблица громкости, расположенная по смещению #110 от начала плеера и занимающая 240 (#F0) байтов. Эта таблица представляет собой 15 строк по 16 значений в строке, каждое значение занимает один байт и является числом от 0 до 15.

В PT3 каждый отсчёт сэмпла имеет громкость от 0 до 15, а сам сэмпл может быть проигран с громкостью от 1 до 15 (нулевая громкость не используется). Вот для определения громкости проигрывания очередного отсчёта по значениям громкости сэмпла и громкости этого отсчёта в сэмпле и нужна таблица. На пересечении строки с номером, равным громкости сэмпла, и столбца с номером, равным громкости отсчёта в сэмпле, и находится искомое значение громкости.

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

Pro Tracker 3 — наиболее широко используемый в настоящее время музыкальный редактор для ZX Spectrum, написанные в нём модули используются во множестве программ (игры, электронные газеты и журналы, intro, demo). Соответственно, область применения такого способа уменьшения длины программы обещает быть весьма широкой.

Ниже приведён участок дампа стандартного плеера Pro Tracker 3.3—3.4, содержащий таблицу громкости.

#0110: 0000 0000 0000 0000 0101 0101 0101 0101
#0120: 0000 0000 0000 0101 0101 0102 0202 0202
#0130: 0000 0000 0101 0101 0202 0202 0303 0303
#0140: 0000 0000 0101 0102 0202 0303 0304 0404
#0150: 0000 0001 0101 0202 0303 0304 0404 0505
#0160: 0000 0001 0102 0203 0303 0404 0505 0606
#0170: 0000 0101 0202 0303 0404 0505 0606 0707
#0180: 0000 0101 0202 0303 0405 0506 0607 0708
#0190: 0000 0101 0203 0304 0505 0606 0708 0809
#01A0: 0000 0102 0203 0404 0506 0607 0808 090A
#01B0: 0000 0102 0303 0405 0606 0708 0909 0A0B
#01C0: 0000 0102 0304 0405 0607 0808 090A 0B0C
#01D0: 0000 0102 0304 0506 0707 0809 0A0B 0C0D
#01E0: 0000 0102 0304 0506 0708 090A 0B0C 0D0E
#01F0: 0001 0203 0405 0607 0809 0A0B 0C0D 0E0F

Я не знаю, какую формулу использовал для расчёта этой таблицы автор Pro Tracker’а, но в точности подошла такая простая формула:

A(i,j)=[(i+1)*j/16].

В этой формуле квадратные скобки обозначают выделение целой части, i — номер строки таблицы (нумеруются с 1), j — номер столбца (нумеруются с 0).

Так как плеер PT3 всегда начинается с адреса #XX00, а таблица расположена по смещению #110 от начала плеера и, таким образом, всегда начинается с адреса #XX10, получается, что координаты i и j элемента таблицы можно получить из младшего байта адреса этого элемента: значение старшего полубайта — это i, а младшего — j.

Учитывая всё вышесказанное, удалось написать достаточно короткий фрагмент программы, строящий таблицу:

        LD   HL,VOL_TAB ;Адрес таблицы - число вида #XX10.
        LD   DE,#000F   ;D - начальное значение i. Проще хранить
                        ;и увеличивать i отдельно, а не
                        ;получать из L. Сейчас i=0, но перед
                        ;вычислением первого элемента таблицы
                        ;i увеличится до 1. E - константа #0F.
;Главный цикл.

M1      LD   A,L        ;Младший байт адреса текущего элемента.
        AND  E          ;=AND #0F - получили j.
        LD   C,A        ;Запомнили j.
        JR   NZ,M2      ;Каждый раз, когда j=0 (т.е. когда
        INC  D          ;начинаем обрабатывать новую строку),
                        ;увеличиваем i на 1.
M2      LD   B,D        ;Счётчик=i.
                        ;Сейчас A=j.
M3      ADD  A,C        ;A=A+j.
        DJNZ M3

;Вычислили A=(i+1)*j. Теперь делим на 16.

        RRCA
        RRCA
        RRCA
        RRCA
        AND  E          ;=AND #0F.
        LD   (HL),A     ;Запись значения элемента.
        INC  L          ;Переход к адресу следующего элемента.
                        ;Если вся таблица заполнена, то L=0.
        JR   NZ,M1      ;L<>0 - переходим к началу главного
                        ;цикла (продолжение заполнения таблицы).

Длина этого фрагмента — всего 25 байтов, а длина исходной таблицы, сжатой с помощью HRUST 1.3, — 94 байта (без учёта служебных 6 байтов получаемого файла, в которых хранится идентификатор «HR», длина исходного блока и длина упакованного блока). Тогда выигрыш от программного построения таблицы составит примерно 94–25=69 байтов (примерно — по нескольким причинам: во-первых, при сжатии всей программы, а не только таблицы, таблица может сжаться не в точности до 94 байтов; во-вторых, сжатая последовательность нулей на месте таблицы тоже займёт сколько-то места, пусть и очень мало; в-третьих, рассматриваемый фрагмент программы также может быть сжат).

Кстати, если известны значения каких-либо регистров к моменту выполнения этого фрагмента, то в некоторых случаях возможна дополнительная оптимизация. Например, если D=0, то можно заменить LD DE,#000F на LD E,#0F, а если E=0, то можно переписать фрагмент так, чтобы поменять местами функции регистров D и E, тогда вместо LD DE,#000F будет LD DE,#0F00, а эту команду уже можно заменить на LD D,#0F. Также при необходимости можно поменять местами функции регистровых пар HL и DE (например, если к моменту выполнения фрагмента в DE уже содержится адрес VOL_TAB).

В плеере Pro Tracker 3.5—3.6 используется несколько другая таблица громкости:

#0110: 0000 0000 0000 0000 0101 0101 0101 0101
#0120: 0000 0000 0101 0101 0101 0101 0202 0202
#0130: 0000 0001 0101 0101 0202 0202 0203 0303
#0140: 0000 0101 0101 0202 0202 0303 0303 0404
#0150: 0000 0101 0102 0202 0303 0304 0404 0505
#0160: 0000 0101 0202 0203 0304 0404 0505 0606
#0170: 0000 0101 0202 0303 0404 0505 0606 0707
#0180: 0001 0102 0203 0304 0405 0506 0607 0708
#0190: 0001 0102 0203 0404 0505 0607 0708 0809
#01A0: 0001 0102 0303 0405 0506 0707 0809 090A
#01B0: 0001 0102 0304 0405 0607 0708 090A 0A0B
#01C0: 0001 0202 0304 0506 0607 0809 0A0A 0B0C
#01D0: 0001 0203 0304 0506 0708 090A 0A0B 0C0D
#01E0: 0001 0203 0405 0607 0708 090A 0B0C 0D0E
#01F0: 0001 0203 0405 0607 0809 0A0B 0C0D 0E0F

В этом случае не так просто подобрать формулу для вычисления элементов таблицы. Но, к счастью, мне удалось найти подпрограмму построения такой таблицы в [1]. Там был приведён исходный текст плеера Pro Tracker 2.101, в котором таблица громкости строится специальной подпрограммой, а при сравнении обнаружилось, что эта таблица — такая же, как в плеере Pro Tracker 3.5—3.6. Кстати, странно: исходный текст плеера в [1] был прокомментирован как «стандартный проигрыватель», а между тем в оригинальном плеере PT2 таблица содержится непосредственно, а не строится подпрограммой.

Приведённую в [1] подпрограмму длиной 43 байта мне удалось существенно оптимизировать, несколько изменив логику её работы и учитывая, что адрес расположения таблицы — число вида #XX10. В результате получилась вот такая подпрограмма длиной 32 байта:

        LD   BC,VOL_TAB ;Адрес таблицы - число вида #XX10.
        XOR  A
        LD   D,A

INITV2  ADD  A,#11      ;Если в результате этого сложения
                        ;получится #100 (т.е. A=0 и флаг C=1),
        SBC  A,D        ;то 0 превратится в #FF после SBC A,D.
        LD   E,A
        LD   H,D
        LD   L,D        ;HL=0 (D всегда равно 0).

INITV1  LD   A,L
        RLA
        LD   A,H
        ADC  A,D        ;=ADC A,0.
        LD   (BC),A
        ADD  HL,DE
        INC  C
        RET  Z
        LD   A,C
        AND  15
        JR   NZ,INITV1

        LD   A,E
        CP   #77
        JR   NZ,INITV2
        INC  A
        JR   INITV2

Чтобы использовать её в виде непосредственно встраиваемого в программу фрагмента (чтобы не тратиться на команду CALL), достаточно заменить команду RET Z на JR Z,M1 (где M1 — адрес следующей за фрагментом команды). Получится фрагмент программы на байт длиннее — 33 байта.

Исходная таблица сжимается с помощью HRUST 1.3 до 97 байтов (также без учёта служебных 6 байтов). Таким образом, примерный выигрыш от программного построения таблицы PT 3.5—3.6 составит 97–33=64 байта.

При рассмотрении этой подпрограммы я понял, что можно написать похожую подпрограмму для построения уже рассмотренной ранее таблицы PT 3.3—3.4:

        LD   BC,VOL_TAB ;Адрес таблицы - число вида #XX10.
        LD   DE,#10

INITV2  LD   HL,#10
        ADD  HL,DE      ;После сложения флаг C сброшен.
        EX   DE,HL      ;DE=DE+#10.
        SBC  HL,HL      ;HL=0.

INITV1  LD   A,H
        LD   (BC),A
        ADD  HL,DE
        INC  C
        RET  Z
        LD   A,C
        AND  15
        JR   NZ,INITV1

        JR   INITV2

Длина этой подпрограммы — 25 байтов, а при оформлении её в виде фрагмента программы — 26 байтов. В сравнении с 25-байтовым фрагментом, приведённым в начале статьи, выигрыша в размере не получилось. Но всё-таки работа была проделана не зря: возникла идея объединить две похожие подпрограммы — эту и предыдущую — в одну универсальную подпрограмму построения таблицы громкости.

Если приходится проигрывать одним и тем же плеером модули, написанные в различных версиях PT3, то желательно перед проигрыванием каждого модуля проверять, в какой версии PT3 он написан, и в соответствии с этим строить нужную таблицу громкости. Тогда каждый модуль будет звучать так, как задумывал его автор. Именно так сделано в моей программе BestView.

Первые байты проигрываемого PT3-модуля представляют собой строку «ProTracker 3.x», где x — номер подверсии. Таким образом, можно, проверив байт, в котором хранится символ x (он находится по смещению 13 от начала модуля), узнать, какую из двух таблиц необходимо сформировать.

Ниже приведён текст универсальной подпрограммы, которая именно это и делает: определяет, какую из двух таблиц надо построить, и строит нужную таблицу. Метка MODULE — это адрес проигрываемого модуля.

        LD   A,(MODULE+13) ;Проверка версии.
        CP   "5"
        LD   HL,#11     ;Подготовка данных для построения
        LD   D,H        ;таблицы PT 3.5-3.6.
        LD   E,H        ;DE=0.
        LD   A,#17      ;Код команды RLA.
        JR   NC,M1      ;Переход, если версия - 3.5 или 3.6.
        DEC  L          ;HL=#10.
        LD   E,L        ;DE=#10.
        XOR  A          ;A=0 (код команды NOP).
M1      LD   (M2),A     ;По адресу M2 будет или NOP, или RLA.

        LD   BC,VOL_TAB ;Адрес таблицы - число вида #XX10.

INITV2  PUSH HL         ;Значение слагаемого (#10 или #11)
                        ;храним в стеке.
        ADD  HL,DE      ;После сложения флаг C сброшен.
        EX   DE,HL      ;DE=DE+#10 или DE=DE+#11, смотря какую
                        ;таблицу строим.
        SBC  HL,HL      ;HL=0.

INITV1  LD   A,L
M2      DB   #7D        ;Здесь будет RLA или NOP.
        LD   A,H

;Если по адресу M2 находится команда NOP, то флаг C сейчас точно
;сброшен (либо после команды SBC HL,HL, либо после команды
;AND 15 в конце цикла), и следующая команда не изменит A.

        ADC  A,0
        LD   (BC),A
        ADD  HL,DE
        INC  C
        LD   A,C
        AND  15
        JR   NZ,INITV1

        POP  HL      ;Восстановили значение слагаемого.
        LD   A,E     ;Если строим таблицу PT 3.3-3.4,
        CP   #77     ;то E никогда не будет равняться #77.
        JR   NZ,M3
        INC  E
M3      LD   A,C     ;Если вся таблица заполнена, то C=0.
        AND  A
        JR   NZ,INITV2

        RET

Длина этой подпрограммы — 53 байта, а при оформлении её в виде фрагмента программы — 52 байта. Как видим, это меньше, чем суммарная длина двух отдельных подпрограмм и команд для определения, какую из них надо вызывать.

Небольшой комментарий к этой подпрограмме. Перед построением таблицы происходит самомодификация кода: по адресу M2 заносится либо код команды NOP, либо код команды RLA, в зависимости от того, какую таблицу надо строить. Как можно видеть, изначально по адресу M2 находится байт #7D. Почему именно он? Потому что он равен значению предыдущего байта фрагмента — коду команды LD A,L. Таким образом, в программе будут два следующих друг за другом одинаковых байта, что положительно скажется на степени её сжатия. Подробнее о таком способе оптимизации рассказывается в [2].

Если таблицу надо строить только один раз, либо если подпрограмма построения таблицы хранится в упакованном виде (возможно, вместе с плеером) и распаковывается заново перед каждым проигрыванием нового модуля, то можно дополнительно её оптимизировать. По адресу M2 тогда можно сразу поместить команду RLA, а из начала подпрограммы убрать команду LD A,#17 и переставить метку M1 на следующую команду (LD BC,VOL_TAB).

Осталось напомнить, что вопрос уменьшения размера программ, в которых используются музыкальные модули (не обязательно написанные в Pro Tracker 3), я уже затрагивал ранее. Так, в [3] рассмотрено улучшения сжатия музыкальных модулей за счёт автоматического определения тех ячеек модуля и плеера, первоначальные значения которых не используются при проигрывании, и заполнения этих ячеек значением из предыдущей используемой ячейки. А в [4], среди прочего, приводится процедура построения частотной таблицы — таким образом, можно не хранить эту таблицу в плеере и за счёт этого улучшить сжатие программы.

Источники

  1. VfNG/NEW. Описание формата модулей Pro Tracker 2.101. Электронная газета «Echo» #2.
  2. И.Рощин. «Улучшение сжатия программ на ассемблере Z80». «Радиомир. Ваш компьютер» 4/2003.
  3. И.Рощин. «Повышение степени сжатия музыкальных модулей». «Радиомир. Ваш компьютер» 7/2002.
  4. И.Рощин. «Частотная таблица с нулевой погрешностью». «Радиолюбитель. Ваш компьютер» 6/2001, «Радиомир. Ваш компьютер» 7,8/2001.

Другие мои статьи об AY-музыке:

1. 

«Правильное изменение частоты огибающей в Pro Tracker 3». «Радиолюбитель. Ваш компьютер» 10,11/2000.

2. 

«Некоторые особенности музыкального сопроцессора». «Радиолюбитель. Ваш компьютер» 11/2000, под псевдонимом BV_Creator.

3. 

«Оптимизация на примере intro „Start“». «Радиомир. Ваш компьютер» 7—10/2001.

(В этой статье можно найти сведения о программировании проигрывания музыки.)

Страница Ивана Рощина > Статьи >