Всем привет. На этот раз я хочу рассказать о том как можно превратить старый телевизор в монитор компьютера. Для этого требуется лишь сам телевизор, HackRF и немного софта.

Работать с HackRF можно с помощью библиоткеки на языке Си. Программы типа SDR# и GNURadio используют именно ее. Чтобы начать передачу нужно подключиться к устройству и как минимум задать рабочую частоту и частоту дискретизации. После начала передачи периодически будет вызываться функция callback, в которой нужно заполнять буфер для передачи (или забирать из него данные если мы принимаем).

int hackrf_start_tx(hackrf_device* device, hackrf_sample_block_cb_fn callback, void* tx_ctx);

Для того чтобы передавать какие-то данные, эти данные должны существовать.
Наиболее простым решением будет использование буфера кадра, в котором лежат уже готовые семплы видеосигнала. Это позволяет максимально уменьшить время выполнения функции callback, т.к. если эта функция закончит свое выполнение после того как внутренний буфер hackRF опустошится, в передаваемом сигнале появятся артефакты.

Так как в телесигнале должны присутствовать синхроимпульсы, они тоже будут находиться в буфере кадров. Еще для того чтобы получить приемлемое разрешение по вертикали, нужно применять черезстрочную развертку. В итоге получилась примерно такая структура кадрового буфера:

image

На этом этапе уже можно выводить какие-нибудь статичные изображения, но это не так интересно. Немного погуглив, я обнаружил пример работы с драйвером виртуального дисплея github.com/LinJiabang/virtual-display
После изучения кода, понял, что функция LJB_VMON_PixelMain отправляет сообщения в UI поток после того как содержимое экрана меняется. Значит можно вызвать функцию наполнения буфера для hackRF в обработчике сообщения winapi WM_PAINT.
После переноса кода из этого проекта в основной и выполнения всех пунктов README получилось заставить винду задетектить виртуальный дисплей и передавать его содержимое в телевизор.

Вывод звука


Кроме того что телевизор умеет показывать изображения, он еще умеет и проигрывать звук.
Для этих целей я тоже поискал готовое решение в виде драйвера виртуальной звуковой карты и нашел scream.
Данный драйвер после установки отправляет по udp сырые семплы аудио на адрес 239.255.77.77:4010. Эти семплы собираются отдельным потоком в кольцевой буфер.
В стандарте SECAM несущая звука идет со смещением относительно видеосигнала на 6.5МГц и передается с частотной модуляцией. Чтобы передать одновременно и изображение и звук, сначала нужно промодулировать звуковой сигнал, затем просто сложить семплы видеосигнала и промодулированного звукового:

image

Так как частота семплирования радиосигнала намного больше чем у звука (в моем случае соотношение получилось 312.5), нужно сделать ресемплинг. Я не стал заморачиваться с интерполяцией, поэтому новый звуковой семпл берется каждые 312.5 семплов hackrf. Так как число дробное, пришлось соорудить простейший delay locked loop (если в аудиобуфере осталось слишком мало семплов, то коэффициент ресемплинга равен 313, а если семплов слишком много, то коэфициент становится равен 312).
В случае если аудиодрайвер не шлёт новых пакетов, буфер опустошается и на вход модулятора подается последний семпл из буфера.

Все вычисления звукового сигнала происходят в fixed-point арифметике, а значения тригонометрических операций получаются табличным методом. Если использовать float-point арифметику и рассчитывать sin и cos в runtime, будет тратиться слишком много процессорного времени. В таблице находится 2048 значений синуса в диапазоне от 0 до 2 Пи. Можно было бы хранить в таблице лишь диапазон от 0 до Пи/2, тогда бы уменьшилось использование памяти, но алгоритм усложнится. В коде это выглядит так:

Исходники

static std::array<int8_t, 2048> calcSinTable()
{
    std::array<int8_t, 2048> result = std::array<int8_t, 2048>();

    for (int i = 0; i < 2048; i++)
    {
        double phase = (((double)i) / 2048.0) * 2.0 * M_PI;

        result[i] = (int8_t)(20 * std::sin(phase));
    }

    return result;
}

static std::array<int8_t, 2048> sinTable = calcSinTable();

uint32_t freqDeviationCoef = (uint32_t) ((1ULL << 32) * (uint64_t)maxFreqDeviation / (uint64_t)sampleRate / 32768);
uint32_t defaultPhaseShift = (1ULL << 32) * (uint64_t)6500000 / (uint64_t)sampleRate;


int SoundProcessor::HackRFcallback(hackrf_transfer* transfer)
{
    int bytes_to_read = transfer->valid_length;
    int bufferUsed = getBufferUsed();

    for (int i = 0; i < bytes_to_read; i += 2)
    {
        signalPhase += defaultPhaseShift + (audioBuf[readAudioPos] * freqDeviationCoef);

        readAudioPosFrac++;
        if (readAudioPosFrac > readAudioDivider)
        {
            readAudioPosFrac = 0;
            if (bufferUsed-- > 0)
            {
                readAudioPos++;
                // размер буфера равен 8192 элементов - степень двойки
                readAudioPos &= 8191;
            }
        }  

        // не нужно проверять переполнение signalPhase, так как оно обрабатывается "как-бы аппаратно" переполнением 32 битной переменной
        int sinPhase = signalPhase >> 21;
	transfer->buffer[i] += (uint8_t)(sinTable[sinPhase]);
        sinPhase -= 512;  // смещаем на 90 градусов
        sinPhase &= 2047; // так как количество элементов таблицы - степень двойки, делать заворот можно просто обнуляя старшие биты
	transfer->buffer[i+1] += (uint8_t)(sinTable[sinPhase]);
    }

    if (bufferUsed < 1900)
        readAudioDivider = 312;

    if (bufferUsed > 2000)
        readAudioDivider = 311;

    return 0;
}



Код как всегда выложен на гитхаб
github.com/rus084/HackRFDisplay

Комментарии (6)


  1. S-trace
    12.11.2021 22:47
    +2

    Любопытно, а DVB-T2 так можно передавать?


    1. bak
      13.11.2021 00:29
      +2

      1. pvvv
        13.11.2021 19:39
        +4

        можно даже и без hackrf
        https://bellard.org/dvbt/
        vga выход — вполне себе ЦАП под 200МГц, а гармониками и до gsm диапазона достать можно:
        https://osmocom.org/projects/osmo-fl2k/wiki


        1. GruBBy_kz
          14.11.2021 12:49
          +2

          Фабрис Беллар и тут отличился, не перестаю удивляться.


  1. sshmakov
    13.11.2021 14:43
    +1

    Между кадровой синхронизацией и экраном должны быть пустые строки. Здесь, кстати, сигналы точного времени передавались и телетекст. Между строчной синхронизацией и экраном должны быть пустые пиксели.


  1. axilirator
    14.11.2021 14:43

    Есть такой проект: https://github.com/fsphil/hacktv. Смотрел раньше с его помощью ролики с YouTube на пузатом телевизоре в съемной квартире. Поддерживает hackRF и не только (можно limeSDR или USRP через драйвер SoapySDR), умеет разные стандарты (PAL, NTSC, SECAM), и даже телетекст! Работает в связке с ffmpeg, соответственно вещать можно много чего. Не уверен, что можно завести под Windows, а вот в каком-нибудь дистрибутиве GNU/Linux - вполне.