На первом этапе будем пытаться реализовывать программный декодер. Это ещё не последняя статья по данной тематике, так как на японских аукционах процессоры могут и закончиться, а PCM должен быть в каждом доме! Найти видик не проблема.
Для работы понадобится файл с записью этих самых QR кодов. Получить его можно при помощи платы видеозахвата. Ну и источник сигнала, разумеется. Можно захватить напрямую выход процессора или же запись на магнитофоне. В идеале, работать сразу с устройством захвата, чтобы декодировать сигнал в реальном времени.
Подойдет любой язык. Начинал я с Python. Но он оказался достаточно медленным на моем ноутбуке, так что в результате перешел на C++. К слову, независимо друг от друга (почти) нашим небольшим сообществом развиваются 3 проекта декодера: на OpenCV (С++), на Qt (С++) и на LabView. О первом и пойдет речь. OpenCV выбрана из-за простоты работы как с устройствами захвата, так и заранее записанными видео. Плюс все манипуляции с изображением там сильно оптимизированы.
Первая проблема, с которой обязательно столкнешься — потерянные данные. Они в любом случае будут и никак этого не избежать без “специализированного” оборудования. PCM использует больше строк, чем помещается в видимую область кадра. В случае с NTSC регионом это число составляет 492 строки на кадр при видимой области в 480. В случае с PAL все куда печальнее.
Интересный факт 1. PCM процессоры в режиме NTSC имеют частоту дискретизации 44,056 kHz, а в PAL привычные нам 44,1 kHz.
Интересный факт 2. Именно мусором в невидимых строках и защищали в свое время VHS кассеты от копирования. Белыми строками сводили с ума АРУ (блок автоматической регулировки уровня). Во время воспроизведения все шло нормально, а вот при записи начинались проблемы. Кстати, некоторые программы для захвата с кассет умеют определять наличие защиты от копирования. Это значит, что содержимое служебных строк все же можно получить. Но сложно.Решений этой проблемы существуют два. Работать с платой захвата хитрым образом в обход драйвера и забирать данные с АЦП, после чего их преобразовывать в полный PCM кадр, или же забить на пропущенные строки. Второй вариант звучит немного дико, но формат хранения данных позволяет восстановить часть данных. В случае с регионом NTSC получается уложиться в ограничения системы коррекции ошибок.
Из-за использования служебных строк нельзя взять видеокарту с композитным выходом и заставить PCM процессор играть. Железо проигнорирует весь кадр, если не найдет заголовок в определенной строке. Есть пара мыслей на этот счет, но об этом как-нибудь потом.
Начнем с того, что видеосигнал идет с чересстрочной разверткой. Каждый кадр содержит в себе как бы два, составленные из нечетных и четных строк. Они называются полями. Именно с полями PCM процессор и работает. Следовательно, и нам нужно разбить исходный поток. Только перед этим черно-белое (оттенки серого) изображение неплохо бы преобразовать в бинарное, чтобы было проще работать.
В этом месте натыкаемся на три трудности, связанные с особенностями устройств видеозахвата. Использовать статический порог для бинаризации изображения нельзя. Но эту проблему решает сам OpenCV, с помощью которого одной волшебной строчкой получаем вполне достойный результат.
threshold(greyFrame, fullFrame, 0, 255, THRESH_BINARY + THRESH_OTSU);
Второй проблемой является, внезапно, цвет. PCM процессоры не используют цветовую составляющую видеосигнала, но платы захвата могут пытаться извлечь её из шумов. Особенно это заметно на самом дешевом EasyCAP. Это может немного испортить результат бинаризации, так что сначала изображение нужно преобразовать к оттенкам серого.
cvtColor(srcFrame, greyFrame, CV_BGR2GRAY);
Кроме вышеупомянутого, EasyCAP умудряется перепутать поля местами. Точнее пропускает первую строку, из-за чего все остальные строки оказываются не на своих местах. Для записи утренника из детского сада это не сильно важно, а вот тут уже становится проблемой. Расставить строки в правильном порядке достаточно легко. В конце каждого кадра есть область без данных. Если мы передвинем строки, содержащие полезный сигнал, вниз до упора, то поля гарантированно вернутся на свои места. При изучении пробовал использовать три устройства захвата из различных ценовых диапазонов, но самым полезным в итоге оказалось самое дешевое, так как оно вскрыло ряд проблем.
На изображении можно наблюдать цветные пятна и более высокий уровень яркости бит данных, если сравнивать с первой иллюстрацией статьи, захваченной на Magewell Pro Capture AIO.
Самое время вспомнить, на чем хранится сигнал. Магнитофоны стандарта 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 анимации.
И так идем до момента, пока последняя ступенька лестницы не упрется в конец поля. Аппаратный PCM имеет циклический буфер. Как только строка была обработана — её можно заполнить новыми данными. Таким образом, последняя ступенька прыгает вверх без прерывания воспроизведения.
Я избрал немного иной принцип работы. Сейчас уже нет такого ограничения на память, так что буфер имеет немного больший размер: высота поля плюс высота лесенки. Как только лесенка доходит до конца буфера — последние 111 строк переносятся в начало, а заполнение новыми данными идет уже со 112 строки. Разумеется, нельзя забывать, что при работе с картой захвата часть строк мы теряем. Поэтому обязательно заполняем недостающие строки нулями, чтобы по ошибкам CRC отметить их для дальнейшего восстановления.
Изначально PCM был 14-битный. Но со временем, когда VHS видеомагнитофоны повысили качество картинки, производители перешли на 16 бит, не забыв при этом про обратную совместимость.
Забавный факт 3. На некоторых 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. Там же есть и генератор.
Сейчас ведется активная работа по допиливанию восстановления по блоку Q, так что для более менее корректной работы придется попрыгать по коммитам. Но это мелочи. Желающие поиграть могут скачать пример захвата.
Wesha
Как же это, оказывается, сложно — записать звук на магнитофон :)