Была ли жизнь до Audio CD? Программный декодер PCM
В прошлой статье мы рассказали про динамические QR коды, которые записывали на VHS кассеты. Эпидемия PCM зацепила и меня, так что пришло время поковырять этот формат.
На первом этапе будем пытаться реализовывать программный декодер. Это ещё не последняя статья по данной тематике, так как на японских аукционах процессоры могут и закончится, а PCM должен быть в каждом доме! Найти видик не проблема.
Для работы понадобится какой-нибудь видеозахват. Без него никуда. Ну и сам источник сигнала, разумеется. Процессор или записанная кассета. В моем случае, была кассета. А дальше как душе угодно. Используйте любой язык. К слову, независимо друг от друга (почти) нашим небольшим сообществом развиваются 3 проекта декодера: на OpenCV (С++), на Qt (С++) и на LabView. О первом далее и пойдет речь. OpenCV выбрана из-за уже реализованных механизмов работы как с устройствами захвата, так и заранее записанными видео. Плюс манипуляции с изображением там сильно оптимизированы.
Главной сложностью при декодировании сигнала являются потерянные данные. Они в любом случае будут и никак этого не избежать без «специализированного» оборудования. PCM использует 490 (для NTSC) строк под данные плюс 2 строки заголовка. Видимый же участок кадра составляет 480 строк, информацию за пределами которых ни одна карта захвата просто так не отдает. В случае с PAL все куда печальнее. Там за пределами видимой области строк ещё больше, так что сегодня будем работать только с NTSC.
Интересный факт. PCM процессоры в режиме NTSC имеют частоту дискретизации 44,056 kHz, а в PAL привычные нам 44,1 kHz.
Решений этой проблемы существуют два. Работать с платой захвата в обход драйвера и забирать данные с АЦП, после чего их преобразовывать в полный PCM кадр, или же забить на пропущенные строки. Второй вариант звучит немного дико, но формат хранения данных позволяет в базовом варианте восстановить до 16 пропущенных подряд строк.
Из-за этой же особенности нельзя взять видеокарту с композитным выходом и заставить PCM процессор играть. Железо ожидает заголовок в определенной строке. Нет заголовка — весь кадр игнорируется. Есть пара мыслей на этот счет, но об этом как нибудь потом.
Начнем с того, что сигнал идет с чересстрочной разверткой. Каждый кадр разбивается на два поля, составленные из нечетных и четных строк. Именно с ними PCM процессор и работает. Следовательно, и нам нужно разбить исходный поток на поля. Только перед этим черно-белое (оттенки серого) изображение неплохо бы преобразовать в бинарное, чтобы было проще работать.
В этом месте натыкаемся на три трудности, связанные с особенностями устройств видеозахвата. Во перых, использовать статический порог для бинаризации изображения нельзя. Но эту проблему решает сам OpenCV. Одной волшебной строчкой получаем достойный результат.
threshold(greyFrame, fullFrame, 0, 255, THRESH_BINARY + THRESH_OTSU);
Второй проблемой явяется, внезапно, цвет. PCM процессоры не используют цветовую составляющую видеосигнала, но платы захвата могут пытаться извлечь её из шумов. Особенно это заметно на самом дешевом EasyCAP. Это может немного поломать алгоритм бинаризации, так что сначала изображение нужно преобразовать к оттенкам серого.
cvtColor(srcFrame, greyFrame, CV_BGR2GRAY);
Кроме этого, EasyCAP умудряется перепутать поля местами, что является третьей проблемой. Но тоже решаемой. В конце каждого кадра есть область без данных. Если мы передвинем строки, содержащие полезный сигнал, вниз до упора, то поля гарантированно вернутся на свои места.
На изображении можно наблюдать цветные пятна и более высокий уровень яркости бит данных, если сравнивать с первой иллюстрацией статьи.
Самое время вспомнить, на чем хранится сигнал. VHS магнитофоны не отличаются особым качеством, так как это все же бытовой формат. Одной только кадровой и строчной синхронизации недостаточно, чтобы однозначно захватить сигнал для дальнейшего воспроизведения. Следовательно, в видеосигнал внесены дополнительные метки для синхронизации. В каждой строке в начале имеется последовательность из чередующихся двух белых и двух черных «пикселей», а в конце строки небольшая область с максимальной яркостью, которая не дает сойти с ума АРУ. Сами же биты данных имеют яркость 60% от максимальной для 1 и менее 20% для 0. Вот пример, почему эти метки необходимы: завороты картинки с кассет в начале и конце кадра.
По синхрометкам в каждой строке находим область данных, считаем ширину бита (всего 128 бит в строке) и ужимаем строку изображения до 16 байт. Рассмотрим поближе формат данных. Строка состоит из 8 блоков по 14 бит, содержащих значения сэмплов и коды коррекции ошибок, и блока с контрольной суммой (CRC-16/CCITT-FALSE). Очевидно, что по контрольным суммам определяются выпавшие строки, данные в которых аппарат попытается восстановить. На каждой строке хранится по три сэмпла для двух каналов, блок четности P (xor всех сэмплов) и загадочное Q. Порядок следующий: L0, R0, L1, R1, L2, R2, P, Q. Про Q сегодня не будем, так как этот функционал ещё не отлажен.
Если использовать «как есть», то побитая строка означает выпадение сразу трех сэмплов, что будет заметно уху по металлическому звону. Но диды были умнее и решили записывать данные лесенками. С одной строки берется только один блок. Следующий берется с небольшим смещением. Ступенька лестницы занимает 16 строк. Блок L0 берется с 1 строки. Блок R0 с 17 строки… Таким образом, с помощью блока четности, можно восстановить данные 16 потерянных подряд строк. Блок Q же позволяет исправить две ошибки, что восстанавливает уже до 32 потерянных строк.
Рассмотрим простой пример. Имеется фрагмент PCM кадра, в котором побились несколько строк (выделены красным). Первые 4 лесенки обработаются нормально. Пятая захватит битую строку. Первым теряется блок Q, но, так как он служит для коррекции ошибок, а сами сэмплы не повреждены, можно идти дальше. С шестой лесенкой поступаем аналогично. Далее снова идут не поврежденные лесенки вплоть до 21. В ней страдает уже блок P. Он тоже служит для восстановления данных. Можно пропустить. Так идем до 37 лесенки, где будет поврежден сэмпл правого канала. Чтобы его восстановить нужно выполнить XOR для блока четности и всех остальных сэмплов:
В результате получим исходное значение. Гораздо сложнее, если в одной лестнице более одной ошибки. Тогда вступает в бой коррекция по Q/, но и она не всемогущая и исправляет максимум две ошибки. Если их больше, то с этим уже ничего не сделать, кроме как интерполировать значения битых сэмплов или обнулить их.
Процесс прохода по полю можно наблюдать на небольшой GIF анимации.
Как можно заметить, почти все данные над первой лесенкой в начале декодирования будут потеряны. Можно восстановить только 16 (32 с Q) из 112 лесенок. Правда особого смысла в этом не вижу.
Когда последняя ступенька лестницы упирается в конец поля, верхняя часть уже обработана и более не нужна. PCM начинает заполнять циклический буфер данными уже из следующего поля. Таким образом и идет непрерывный процесс декодирования. Немного иной принцип имеет программный декодер (костыль). Тут уже нет такого ограничения на память (в те времена она была дорогой), так что буфер имеет немного больший размер: 245 строк плюс высота лесенки в 111. Как только уперлись в конец буфера — перенесли последние строки в начало и со 112 строки заполняем данными уже из следующего поля.
Разумеется, нельзя забывать, что на этапе видеозахвата часть строк мы теряем. Поэтому обязательно заполняем эти строки нулями, чтобы по ошибкам CRC отметить их для дальнейшего восстановления. Иначе получим кашу.
Изначально PCM был 14-битный. Но со временем, когда VHS видеомагнитофоны повысили качество картинки, производители перешли на 16 бит, не забыв при этом про обратную совместимость.
Забавный факт. Во некоторых 14-битных PCM процессорах стояли 12 битные АЦП. А два недостающих бита были просто запараллелены со старшим (он же отвечает за знак).
В 16-битном PCM блока Q вообще нет, так что в заголовке поля имеется специальная отметка «коррекция по Q невозможна». Вместо него собраны по 2 недостающих бита сэмплов и P. Высота лесенки в данном случае уже не 8 ступенек, а всего 7, так как недостающие биты блока хранятся на его же строке, а не отдельно. Понять, как устроен 16-битный PCM достаточно просто на примере захвата меандра частотой в 100 Герц и максимальной амплитудой. Все сразу встает на свои места.
Теперь самое время сохранить результат в wav файл. Поможет в этом библиотека libsndfile. Хотя… PCM же не сохраняет файлы, а сразу же воспроизводит. Тут можно вспомнить про такую классную штуку, как pipe. Когда вывод одной программы поступает на вход другой. Просто указываем stdout как назначение и перенаправляем поток в программу ffplay.
./ggg -i easycap.avi -o - | ffplay -
Теперь можно наслаждаться выпадениями и продолжать отлаживать код, чтобы от них избавиться…
На этом на сегодня все. Скачать исходники декодера можно со странички на GitHub: https://github.com/walhi/pcm. Там же есть и генератор. Когда нибудь я оформлю его как плагин для foobar…
Сейчас ведется активная работа по допиливанию восстановления по блоку Q, так что для более менее корректной работы придется попрыгать по коммитам. Но это мелочи. Желающие поиграть могут скачать пример захвата.