© Иван Рощин, Москва
| ZXNet | : 500:95/462.53 |
| : bestview@mtu-net.ru | |
| WWW | : http://www.ivr.da.ru |
(«Радиомир. Ваш
компьютер» 7/2002)
Дата последнего редактирования: 1.04.2003.
Введение
О музыкальных модулях
Основная идея
Реализация
Руководство пользователя
Получаемый выигрыш
О скорости эмуляции
Ограничения эмуляции
Скачать текст программы (8 КБ ZIP)
Чтобы программа занимала меньше места на диске, её часто сжимают
Для повышения степени сжатия модулей можно использовать специальный
алгоритм сжатия, учитывающий их структуру. Тут, однако, есть свои недостатки.
Но есть другой способ так преобразовать модуль, чтобы он более эффективно сжался обычным упаковщиком, тем же, которым сжимается вся программа. Тогда проблемы, связанные с необходимостью наличия специального распаковщика, отпадут. Также, как мы увидим дальше, не возникнет необходимости подробно разбираться в формате модулей. Об этом способе и пойдёт речь.
Как известно, музыку пишут с помощью специальных программ музыкальных редакторов. Чтобы использовать написанную мелодию отдельно от редактора, её подвергают специальной обработке компиляции. Компилятор может быть как встроенным в редактор, так и независимым.
Музыкальный модуль может быть откомпилирован без плеера (тогда там будут только данные о мелодии) или с плеером, т.е. с набором процедур для проигрывания (тогда в модуле сначала идёт плеер, а за ним данные). Длина плеера, как правило, постоянна для данного компилятора, а длина данных зависит от конкретной мелодии.
Для вызова процедур плеера (обычно таких процедур три) используются точки входа, находящиеся по определённому смещению от начала плеера.
Первая процедура INIT (инициализация), вызывается перед началом проигрывания.
Вторая процедура PLAY (проигрывание), вызывается каждые 1/50 секунды (обычно синхронно с маскируемыми прерываниями). Время работы этой процедуры составляет от нескольких процентов до нескольких десятков процентов от промежутка между двумя прерываниями (в зависимости от редактора, в котором создан модуль, и от используемого плеера), так что на проигрывание музыки тратится лишь небольшая часть времени процессора.
Третья процедура STOP (заглушение звука). Она может и не использоваться в конкретной программе.
И в данных о мелодии, и в плеере первоначальные значения некоторых ячеек
памяти могут не использоваться при проигрывании (назовём такие ячейки
неиспользуемыми). То есть либо к этим ячейкам вообще не происходит
обращения, либо до чтения в них
В данных о мелодии не использоваться при проигрывании могут, например, название мелодии или данные о конце длинного сэмпла (если мелодия такова, что после одной ноты через короткий промежуток времени следует другая, и в результате конец сэмпла никогда не проигрывается). В плеере неиспользуемыми могут быть некоторые элементы частотной таблицы (где указывается значение делителя частоты для каждой ноты): ведь, как правило, в мелодии используются не все ноты. Также в плеере могут не использоваться некоторые обработчики так называемых команд (не путать с командами процессора), таких как «изменить темп», «изменить частоту ноты»: ведь обычно в мелодии присутствуют не все возможные команды. Может не использоваться первоначальное значение определённых ячеек памяти, выделенных для хранения переменных при работе плеера. Может быть и другая неиспользуемая информация.
Так вот, если знать все неиспользуемые ячейки модуля, то их можно обнулить, и тогда модуль сожмётся гораздо лучше: ведь обычно такие ячейки образуют последовательности, а последовательности одинаковых байтов хорошо сжимаются.
Ещё
А как узнать адреса неиспользуемых ячеек, ничего не зная о формате модуля? Это можно сделать, эмулируя действия процессора при работе плеера и отслеживая при этом все операции с памятью.
Отмечу, что такой способ определения неиспользуемых ячеек памяти может быть использован не только для ZX Spectrum и не только для обработки музыкальных модулей.
Для определения неиспользуемых ячеек заведём в памяти массив с количеством
элементов, равным длине обрабатываемого модуля. Сначала обнулим все элементы.
Нулевое значение будет обозначать, что к соответствующей ячейке памяти не было
обращения. При эмуляции, если произошло обращение к
Если после окончания эмуляции элемент массива равен нулю значит, к соответствующей ячейке модуля не было обращений; если элемент равен 1 значит, значение этой ячейки было прочитано; если равен 2 значит, первоначальное значение не было прочитано, а на его место было записано другое. Понятно, что ячейки, которым соответствуют значения 0 и 2, и будут искомыми.
Теперь более подробно об эмуляции. При проигрывании музыки вначале
вызывается процедура INIT, затем процедура PLAY (определённое
количество раз), и, если надо, процедура STOP. Рассмотрим, как производится
эмуляция действий процессора при выполнении
Имеются следующие структуры данных: область памяти, в которой располагается стек выполняемой процедуры, и массив, где хранятся значения регистров при её выполнении (так называемые эмулируемые регистры; их я буду в дальнейшем обозначать, добавляя «EM» к названию регистра: например, EM_HL). Перед выполнением процедуры в вышеописанный массив записываются начальные значения регистров, при этом в EM_SP помещается адрес конца области памяти, выделенной под стек (не забываем: стек растёт вниз), а в EM_PC адрес начала процедуры.
Выполнение происходит по отдельным командам. Рассмотрим выполнение одной команды. Значения регистров на момент её выполнения известны. Адрес начала команды также известен (он содержится в EM_PC).
Если выполняемая команда не является командой передачи управления, то она копируется в специальный буфер и непосредствено запускается на выполнение. Перед этим содержимое массива регистров загружается в регистры процессора, а после выполнения значения регистров процессора записываются в массив. EM_PC увеличивается на длину выполненной команды и, таким образом, указывает теперь на следующую команду.
Выполнение команды передачи управления происходит с помощью специальной подпрограммы (для каждой такой команды подпрограмма своя).
Если текущая выполняемая команда RET, и при этом значение EM_SP совпадает с адресом конца области памяти, выделенной под стек, то считается, что эта команда обеспечивает выход из выполняемой процедуры. На этом выполнение процедуры завершается.
Текст программы обработки музыкальных модулей, основанной на описанном выше принципе, вы можете скачать, воспользовавшись ссылкой в начале статьи, а здесь я расскажу, как этой программой пользоваться.
Сразу же замечу, что адреса, по которым располагается как сама программа, так и структуры данных, можно при необходимости изменить (например, если по тем же адресам должен располагаться обрабатываемый модуль).
Итак, вам нужно обработать модуль с плеером или без (если модуль без плеера, то плеер должен быть загружен отдельно). Вам должна быть известна следующая информация: адрес и длина модуля, адреса процедур INIT, PLAY и STOP плеера (если STOP не используется адрес считается равным #0052), количество вызовов процедуры PLAY (если модуль зациклен и проигрывается «бесконечно», то количество вызовов устанавливается таким, чтобы цикл был проигран два раза).
Так как для хранения количества вызовов процедуры PLAY используется
двухбайтная переменная, количество вызовов не может превышать 65535. Исходя из
того, что между двумя вызовами проходит 1/50 секунды, получаем, что
максимальное время проигрывания модуля 21 минута и 50,7 секунды. Я
не стал усложнять программу, выделяя под переменную три байта для увеличения
максимального времени проигрывания, так как для подавляющего большинства
модулей это время не превышает нескольких минут. Тем не менее, изменение
программы при необходимости не представляет
Итак, устанавливаете значения соответствующих переменных в тексте программы, компилируете её, затем загружаете модуль и вызываете процедуру «эмуляция с обнулением массива RW_MEMORY» (это массив с информацией об используемых ячейках памяти). Для вызова этой процедуры используется точка входа по смещению 0 от начала программы.
Эмуляция займёт довольно продолжительное время, при этом вы будете слышать замедленное проигрывание мелодии. Если после её окончания флаг C будет сброшен, то всё в порядке; если же установлен значит, при эмуляции встретилась неподдерживаемая недокументированная команда (см. раздел «Ограничения эмуляции»). Адрес этой команды вы сможете узнать из переменной EM_PC.
После окончания эмуляции модуль следует перезагрузить: вдруг при его
проигрывании значения
Когда нужно обработать один за другим несколько модулей, удобнее
устанавливать значения переменных не в исходном тексте программы с последующей
перекомпиляцией, а непосредственно в коде программы с помощью отладчика.
Смещения переменных от начала программы приведены в табл. 1. При
установке значений не забывайте, что они должны быть в формате
| Переменная (2 байта, |
Смещение от начала программы |
|---|---|
| Адрес модуля | 6 |
| Длина модуля | 8 |
| Адрес массива с информацией об используемых ячейках памяти | 10 |
| Адрес вершины стека для эмуляции (стек растёт вниз!) | 12 |
| Адрес процедуры INIT | 14 |
| Адрес процедуры PLAY | 16 |
| Количество вызовов процедуры PLAY | 18 |
| Адрес процедуры STOP | 20 |
Если в вашей программе используются несколько модулей без плеера,
проигрываемые отдельным плеером, и вы хотите обработать только этот плеер, то
порядок действий следующий. Устанавливаете значения соответствующих переменных
в тексте программы, подставляя при этом вместо адреса начала модуля и длины
модуля соответственно адрес начала плеера и длину плеера. Компилируете
программу, в отладчике загружаете плеер и первый музыкальный модуль. Вызываете
процедуру «эмуляция с обнулением массива RW_MEMORY». После
окончания эмуляции загружаете второй модуль и вызываете процедуру
«эмуляция без обнуления массива RW_MEMORY» (точка входа по
смещению 2 от начала программы). Так же повторяете и со всеми остальными
модулями. Так как очистка массива с информацией об используемых ячейках памяти
не производится, информация в нём будет накапливаться: если при проигрывании
хотя бы одного модуля значение
А стоит ли овчинка выделки? Ещё бы! :) Для примера я взял десять
| Файл | Длина, байтов | Старая длина после упаковки | Новая длина после упаковки | Выигрыш | ||||
|---|---|---|---|---|---|---|---|---|
| байтов | % от исходной | байтов | % от исходной | байтов | % от исходной длины | % от старой длины после упаковки | ||
| F_ANTROP.C | 5963 | 3258 | 55 | 2521 | 42 | 737 | 12 | 23 |
| IN2LIGHT.C | 11404 | 4479 | 39 | 3583 | 31 | 896 | 8 | 20 |
| squeak.C | 12871 | 5809 | 45 | 5269 | 41 | 540 | 4 | 9 |
| i'veCCCC.C | 10226 | 4185 | 41 | 3491 | 34 | 694 | 7 | 17 |
| SUMMER.C | 13239 | 5905 | 45 | 5436 | 41 | 469 | 4 | 8 |
| LostMemo.C | 6739 | 3507 | 52 | 2877 | 43 | 630 | 9 | 18 |
| Another.C | 5627 | 3151 | 56 | 2453 | 44 | 698 | 12 | 22 |
| FroznABC.C | 12800 | 5683 | 44 | 5272 | 41 | 411 | 3 | 7 |
| simpnice.C | 5357 | 2942 | 55 | 2102 | 39 | 840 | 16 | 29 |
| spacefly.C | 6885 | 3627 | 53 | 2953 | 43 | 674 | 10 | 19 |
Если посчитать средний выигрыш, получится 8,5% от исходной длины модуля, то
есть для
В табл. 3 приведена аналогичная информация для десяти
| Файл | Длина, байтов | Старая длина после упаковки | Новая длина после упаковки | Выигрыш | ||||
|---|---|---|---|---|---|---|---|---|
| байтов | % от исходной | байтов | % от исходной | байтов | % от исходной длины | % от старой длины после упаковки | ||
| ARCTLAND.m | 4318 | 1736 | 40 | 1662 | 38 | 74 | 2 | 4 |
| HAPPYLIL.m | 3408 | 1189 | 35 | 1089 | 32 | 100 | 3 | 8 |
| TO STAR.m | 5177 | 1798 | 35 | 1715 | 33 | 83 | 2 | 5 |
| SURPRIZE.m | 6495 | 1515 | 23 | 1445 | 22 | 70 | 1 | 5 |
| flies.m | 4896 | 1849 | 38 | 1784 | 36 | 65 | 1 | 4 |
| Zvaigzde.m | 4438 | 2193 | 49 | 1929 | 43 | 264 | 6 | 12 |
| SEA.m | 3036 | 1457 | 48 | 1381 | 45 | 76 | 3 | 5 |
| ACID GOA.m | 3930 | 1479 | 38 | 1036 | 26 | 443 | 11 | 30 |
| bugsMind.m | 4176 | 1567 | 38 | 1489 | 36 | 78 | 2 | 5 |
| PEACE.m | 7124 | 2297 | 32 | 2146 | 30 | 151 | 2 | 7 |
Как видим, здесь средний выигрыш составляет около 3% от длины исходного
модуля, т.е. для
При эмуляции выполнение программы сильно замедляется в среднем
в 125 раз (для разных команд замедление неодинаково). Это не значит, однако,
что
Да, быстрым этот процесс не назовёшь. Каждую выполняемую команду требуется распознать, определить способ её выполнения (непосредственным запуском или с помощью специальной подпрограммы), узнать, происходит ли при её выполнении чтение из памяти или запись в память (и по каким адресам) и изменить при необходимости содержимое соответствующего элемента (элементов) массива, где хранится информация об обращении к ячейкам. На это и уходит время.
Чтобы повысить скорость эмуляции, можно вместо того, чтобы всегда выполнять программу по отдельным командам, во время выполнения выявлять участки программы, в которых нет команд перехода за пределы участка и либо нет команд чтения/записи, либо есть, но адреса операндов фиксированы (тогда информация об обращении к соответствующим ячейкам памяти будет обработана при первом выполнении участка). Также в участке не должна происходить самомодификация кода. Тогда при повторном выполнении такого участка не будет обращения ни к одной ячейке памяти кроме тех, к которым было обращение при его первом выполнении. Таким образом, вместо покомандного выполнения достаточно скопировать этот участок в буфер и непосредственно запустить на выполнение, что будет значительно быстрее.
При этом, правда, появятся дополнительные затраты времени на определение таких участков и занесение информации о них в специальный список, на определение того, что началось выполнение такого участка, на отслеживание записи в участки (если произошла такая запись, код участка может быть модифицирован, и такой участок должен быть исключён из списка). Так что, возможно, повышение скорости эмуляции не окажется значительным. Но это я предлагаю проверить самостоятельно.
Так как программа предназначена для выполнения довольно специфической
задачи обработки музыкальных модулей, то эмуляция Z80 была
несколько упрощена. Но если вы захотите обрабатывать с помощью этой программы
не музыкальные модули, а
|
Другие мои статьи о сжатии:
|
|
Другие мои статьи об
|