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. Заголовок модуля
| Смещение | Размер | Поле | Описание |
|---|---|---|---|
| +0 | 1 байт | Tempo/Delay | Базовый темп (число тиков на 1 квант) |
| +1 | 1 байт | Length | Количество позиций |
| +2 | 1 байт | Loop | Позиция зацикливания |
| +3 | 64 байта | SampleOffsets[32] | Смещения к 32 сэмплам (LE) |
| +67 | 32 байта | OrnamentOffsets[16] | Смещения к 16 орнаментам (LE) |
| +99 | 2 байта | PatternsOffset | Смещение к таблице паттернов (LE) |
| +101 | 30 байт | 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/NOP | 0x00 | Конец данных канала (или NOP внутри строки) |
| SAMPLE | 0xE1-0xFF | Установить сэмпл |
| REST/SHUT UP | 0xE0 | Выключить канал |
| NOTE ON | 0x80-0xDF | Включить ноту |
| ENVELOPE OFF | 0x7F | Отключить огибающую |
| SET ENVELOPE | 0x71-0x7E | Установить огибающую + 2 байта периода (LE) |
| QUIT | 0x70 | Выйти из строки паттерна |
| SET ORNAMENT | 0x60-0x6F | Установить орнамент |
| SET SKIP | 0x20-0x5F | Установить период пропуска (сколько квантов ждать) |
| SET VOLUME | 0x10-0x1F | Установить громкость канала (0-15) |
| SET DELAY | 0x0F | Изменить темп (следующий байт) |
| GLISS | 0x0E/0x0D | Глиссандо / Портоаменто |
| GLISS OFF | 0x0C | Отключить глиссандо |
| NOISE ADD | 0x01-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. Механизм воспроизведения
- Инициализация: Загружается заголовок, устанавливаются адреса каналов для первой позиции
- Главный цикл (вызывается каждый тик):
- Декремент общего счётчика задержки
- Если задержка истекла — обработка каналов
- Для каждого канала: декремент счётчика пропуска, если 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) & 0xFF) << 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 << 4) | Volume) >> 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) & 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/NEW | Bulba | ZXTune |
|---|---|---|---|
| SAMPLE индекс | ((op+0x20)&0xFF)/2 | ((op+0x20)&0xFF)*2/2 | op - 0xE0 |
| NOTE нота | (op+0x20+0x60)&0xFF | (op+0x20+0x60)&0xFF | op - 0x80 |
| SKIP значение | adjusted | adjusted + 1 | op - 0x20 |
| 0xE0 | SAMPLE 0 (заглушение) | PD_REL (флаги) | REST (выключить) |
| 0x70 | ENVELOPE ? | PD_QUIT | QUIT |
| 0x7F | ENVELOPE ? | PD_EOff | ENVELOPE OFF |
| PORTAMENTO | 3 байта | 3 байта (пропуск дельты) | 3 байта (step + limit) |
| 0x0C | NOISE ADD | PD_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. Рекомендации по реализации
- Для максимальной совместимости используйте алгоритм Bulba (наиболее распространённый в плеерах ZX Spectrum)
- Индекс сэмпла: используйте ((op + 0x20) & 0xFF) / 2 (VfNG/Bulba) — ZXTune-метод несовместим
- Ноту: используйте цепочку сложений (VfNG/Bulba) — ZXTune-метод даёт другие ноты
- SKIP: cmd - 0x1F (Bulba) — добавляет 1 относительно ZXTune
- Знак вибрато: 0 = отрицательный, 1 = положительный (VfNG/Bulba) — ZXTune инвертирован
- Volume Table: ((Level << 4) | Volume) >> 4 (Bulba) — инвертированный индекс
- Проверка 0x00: только для канала A (все версии)
- Счётчики: декремент всех трёх каналов при DelyCnt==0 (Bulba)
- 0x0C: PD_STOP (Bulba) или GLISS OFF (ZXTune) — не NOISE ADD (VfNG)