1. Общая структура файла​

PT2 — скомпилированный музыкальный модуль редактора Pro Tracker 2.x для ZX Spectrum (чип AY-3-8910/YM2149F). Файл содержит все данные для воспроизведения: паттерны, сэмплы (описания поведения каналов), орнаменты (отклонения нот) и список позиций.

Порядок секций в файле:​

Код:
+0x00:    Заголовок (Tempo, Length, Loop, таблицы смещений, название)
+0x83:    Position List (список номеров паттернов, заканчивается 0xFF)
+?:       Pattern Pointers Table (6 байт на паттерн, заканчивается 0x0000)
+?:       Pattern Data (потоки команд для каналов)
+?:       Samples (описания кварков)
+?:       Ornaments (таблицы отклонений нот)

2. Заголовок модуля​

СмещениеРазмерПолеОписание
+01 байтTempo/DelayБазовый темп (число тиков на 1 квант)
+11 байтLengthКоличество позиций
+21 байтLoopПозиция зацикливания
+364 байтаSampleOffsets[32]Смещения к 32 сэмплам (LE)
+6732 байтаOrnamentOffsets[16]Смещения к 16 орнаментам (LE)
+992 байтаPatternsOffsetСмещение к таблице паттернов (LE)
+10130 байтNameНазвание (ASCII, до 30 символов)
+131...Positions[]Список позиций


Смещения Sample/Ornament: абсолютные, от +0, LE. Сэмпл 0 = заглушение канала. Орнамент 0 = отсутствие орнамента.


3. Position List​

Начинается с +131. Байтовый массив номеров паттернов. Заканчивается 0xFF. Максимум 255 позиций.


4. Pattern Pointers Table​

Начинается с PatternsOffset. Каждый паттерн = 6 байт (3 канала × 2 байта LE). Заканчивается 0x0000.
Код:
PatternPtr[PatternIndex * 6 + 0] = смещение канала A (LE)
PatternPtr[PatternIndex * 6 + 2] = смещение канала B (LE)
PatternPtr[PatternIndex * 6 + 4] = смещение канала C (LE)

Смещения абсолютные, от начала файла.


5. Команды каналов (Pattern Data)​

Поток байтов. Декодирование while (offset < size) { cmd = *offset++; ... }.

Таблица команд:​

КомандаКодДействие
END/NOP0x00Конец данных канала (или NOP внутри строки)
SAMPLE0xE1-0xFFУстановить сэмпл
REST/SHUT UP0xE0Выключить канал
NOTE ON0x80-0xDFВключить ноту
ENVELOPE OFF0x7FОтключить огибающую
SET ENVELOPE0x71-0x7EУстановить огибающую + 2 байта периода (LE)
QUIT0x70Выйти из строки паттерна
SET ORNAMENT0x60-0x6FУстановить орнамент
SET SKIP0x20-0x5FУстановить период пропуска (сколько квантов ждать)
SET VOLUME0x10-0x1FУстановить громкость канала (0-15)
SET DELAY0x0FИзменить темп (следующий байт)
GLISS0x0E/0x0DГлиссандо / Портоаменто
GLISS OFF0x0CОтключить глиссандо
NOISE ADD0x01-0x0BДобавка к частоте шума (следующий байт)

6. Сэмплы (Samples)​

Не PCM-звук, а описатели поведения AY-канала.

Структура:​

Код:
+0: Size   — количество кварков (1 байт)
+1: Loop   — точка зацикливания (1 байт)
+2: Quarks[Size] — кварки по 3 байта каждый

Кварк (3 байта):​

Код:
Byte 0 — NoiseAndFlags:  nnnnnsTN
  bits 7-3: n — частота шума (0-31)
  bit 2:    s — знак вибрато
  bit 1:    T — маска тона (1=выключен)
  bit 0:    N — маска шума (1=выключен)

Byte 1 — LevelHiVibrato: aaaaHHHH
  bits 7-4: a — амплитуда/уровень (0-15)
  bits 3-0: H — старшие 4 бита вибрато

Byte 2 — LoVibrato: LLLLLLLL
  bits 7-0: L — младшие 8 бит вибрато

Vibrato = ((H << 8) | L) × (sign)

7. Орнаменты (Ornaments)​

Таблицы отклонений от ноты в полутонах.

Структура:​

Код:
+0: Size   — количество элементов (1 байт)
+1: Loop   — точка зацикливания (1 байт)
+2: Values[Size] — знаковые байты (-128..+127)

8. Механизм воспроизведения​

  1. Инициализация: Загружается заголовок, устанавливаются адреса каналов для первой позиции
  2. Главный цикл (вызывается каждый тик):
    • Декремент общего счётчика задержки
    • Если задержка истекла — обработка каналов
    • Для каждого канала: декремент счётчика пропуска, если 0 — декодирование команд
    • Вычисление AY-регистров (частота, амплитуда, шум, микшер)
    • Вывод в порты AY-чипа

9. AY-3-8910 Регистры​

Код:
R0-R1:  Тон канала A (12 бит)
R2-R3:  Тон канала B
R4-R5:  Тон канала C
R6:     Частота шума (5 бит)
R7:     Микшер (биты 0-2: тон, биты 3-5: шум; 1=выкл, 0=вкл)
R8:     Амплитуда канала A (биты 0-3: громкость 0-15, бит 4: 1=огибающая)
R9:     Амплитуда канала B
R10:    Амплитуда канала C
R11-12: Период огибающей (16 бит)
R13:    Форма огибающей (0-15, 255=выкл)

10. Сравнение трёх версий​

10.1. VfNG/NEW (оригинальная документация)​

Источник: Текст VfNG/NEW с описанием формата PT2.101 и ассемблерный плеер.

Особенности:
  • Счётчики каналов: 3 раздельных (LF748, LF749, LF74A), декрементируются каждый в своём тике по очереди (A→B→C)
  • 0x00 = отключение канала (L11)
  • 0xE0-0xFF → SAMPLE: A = (op + 0x20), индекс = A/2
  • 0x80-0xDF → NOTE: A = (op + 0x20 + 0x60), нота = A
  • 0x70-0x7F → ENVELOPE: 0x70 = выход
  • 0x0D → PORTAMENTO: 3 байта (шаг, дельта_lo, дельта_hi)
  • 0x0E → GLISSADE: 1 байт (signed)
  • 0x0F → DELAY: 1 байт
  • 0x01-0x0B → ADD NOISE: 1 байт
  • Знак вибрато: 0 = "+", 1 = "-"
  • Volume Table: генерируется процедурой INITVOL (256 байт)
  • Проверка конца паттерна: 0x00 в начале данных канала A

10.2. Bulba Universal PT2 Player​

Источник: (c)2005 S.V.Bulba, ассемблерный плеер для ZX Spectrum.

Особенности:
  • Счётчики: 3 раздельных (SkpCnt для каждого канала), декрементируются все при DelyCnt==0
  • 0xE0 → PD_REL (установка флагов канала)
  • 0xE1-0xFF → PD_SAM: A = ((op + 0x20) &amp; 0xFF) &lt;&lt; 1, индекс = A/2
  • 0x80-0xDF → PD_NOTE: нота через цепочку сложений (аналогично VfNG)
  • 0x7F → PD_EOff (отключить огибающую)
  • 0x71-0x7E → PD_ENV (установить огибающую)
  • 0x70 → PD_QUIT
  • 0x0D → PD_PORT: 3 байта, но пропускает предрассчитанную дельту (INC BC / INC BC)
  • 0x0E → PD_GLIS
  • 0x0C → PD_STOP (остановить глиссандо/портаменто)
  • 0x01-0x0B → добавка к шуму
  • PD_SKIP: Skip = adjusted + 1 (INC A перед сохранением)
  • Volume Table: ((Level &lt;&lt; 4) | Volume) &gt;&gt; 4 — инвертированный индекс относительно VfNG!
  • Проверка 0x00: только в канале A, каналы B/C декодируются без проверки
  • Глобальный пропуск линий: через GetMinCounter() (все каналы вместе)
  • Знак вибрато: как в VfNG (0="-", 1="+")
  • Таблица частот: генерируется динамически (NTBL → NT_)
  • PT2.4PF: поддержка относительных смещений (коррекция через MODADDR)

10.3. ZXTune (витамин/vitamin.​

Источник: Открытый исходный код проекта ZXTune (C++).

Особенности:
  • SAMPLE: cmd - 0xE0 (прямой индекс) — отличается от VfNG/Bulba!
    • VfNG/Bulba: индекс = ((op + 0x20) &amp; 0xFF) / 2
    • ZXTune: индекс = op - 0xE0
    • Это даёт РАЗНЫЕ индексы для нечётных op!
  • NOTE: cmd - 0x80 — отличается от VfNG/Bulba!
    • VfNG/Bulba: нота через цепочку сложений
    • ZXTune: нота = cmd - 0x80
    • Разница в 16 полутонов для одного и того же байта!
  • SKIP: cmd - 0x20 — отличается от Bulba!
    • Bulba: cmd - 0x1F (INC A)
    • ZXTune: cmd - 0x20
  • ENVELOPE: 0x71-0x7E (тип 1-14), 0x70 = QUIT
  • GLISS: 0x0D = NOTE GLISS (портаменто с лимитом), 0x0E = простой GLISS
  • Счётчики: глобальный пропуск через GetMinCounter() (как Bulba)
  • Конец паттерна: 0x00 только для канала A в HasLine()
  • Знак вибрато: ИНВЕРТИРОВАН! 1 = "+", 0 = "-" (противоположно VfNG/Bulba!)
  • Sample Loop: std::min(Loop, Size) — всегда валидный
  • Проверка смещений: minOffset = PatternsOffset + MaxPattern * sizeof(RawPattern)

11. Сводная таблица различий​

ПараметрVfNG/NEWBulbaZXTune
SAMPLE индекс((op+0x20)&0xFF)/2((op+0x20)&0xFF)*2/2op - 0xE0
NOTE нота(op+0x20+0x60)&0xFF(op+0x20+0x60)&0xFFop - 0x80
SKIP значениеadjustedadjusted + 1op - 0x20
0xE0SAMPLE 0 (заглушение)PD_REL (флаги)REST (выключить)
0x70ENVELOPE ?PD_QUITQUIT
0x7FENVELOPE ?PD_EOffENVELOPE OFF
PORTAMENTO3 байта3 байта (пропуск дельты)3 байта (step + limit)
0x0CNOISE ADDPD_STOP (no gliss)GLISS OFF
Знак вибрато0="+", 1="-"0="-", 1="+"1="+", 0="-"
Volume Table индекс(Vol<<4)|Level((Lvl<<4)|Vol)>>4— (внешний)
Проверка 0x00Только канал AТолько канал AТолько канал A в HasLine
Счётчики каналовПо очереди (A→B→C)Все сразу при DelyCnt=0Все сразу с min skip
Генерация таблицы частотСтатическая TONE_TBДинамическая (NTBL→NT_)— (внешняя)

12. Рекомендации по реализации​

  1. Для максимальной совместимости используйте алгоритм Bulba (наиболее распространённый в плеерах ZX Spectrum)
  2. Индекс сэмпла: используйте ((op + 0x20) &amp; 0xFF) / 2 (VfNG/Bulba) — ZXTune-метод несовместим
  3. Ноту: используйте цепочку сложений (VfNG/Bulba) — ZXTune-метод даёт другие ноты
  4. SKIP: cmd - 0x1F (Bulba) — добавляет 1 относительно ZXTune
  5. Знак вибрато: 0 = отрицательный, 1 = положительный (VfNG/Bulba) — ZXTune инвертирован
  6. Volume Table: ((Level &lt;&lt; 4) | Volume) &gt;&gt; 4 (Bulba) — инвертированный индекс
  7. Проверка 0x00: только для канала A (все версии)
  8. Счётчики: декремент всех трёх каналов при DelyCnt==0 (Bulba)
  9. 0x0C: PD_STOP (Bulba) или GLISS OFF (ZXTune) — не NOISE ADD (VfNG)