TV signal transmission via HackRF

Hello everyone. This time I want to talk about how you can turn an old TV into a computer monitor. This requires only the TV itself, HackRF and some software.

You can work with HackRF using

C libraries

… Programs like SDR # and GNURadio use it. To start transmission, you need to connect to the device and at least set the operating frequency and sampling frequency. After the start of the transfer, the callback function will be periodically called, in which you need to fill the buffer for transfer (or take data from it if we receive).

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

In order to transmit any data, this data must exist.

The simplest solution would be to use a frame buffer, which contains ready-made video samples. This allows you to minimize the execution time of the callback function as much as possible. if this function finishes its execution after the internal hackRF buffer is empty, artifacts will appear in the transmitted signal.

Since sync pulses must be present in the TV signal, they will also be in the frame buffer. In order to get an acceptable vertical resolution, you need to use interlaced scanning. As a result, we got something like the following frame buffer structure:

At this stage, you can already display some static images, but this is not so interesting. After a little googling, I found an example of working with the virtual display driver
After studying the code, I realized that the LJB_VMON_PixelMain function sends messages to the UI thread after the screen content changes. This means you can call the function of filling the buffer for hackRF in the winapi WM_PAINT message handler.
After transferring the code from this project to the main one and completing all the README items, it turned out to force Windows to detect the virtual display and transfer its contents to the TV.

Sound output

In addition to the fact that the TV can display images, it can also play sound.

For these purposes, I also looked for a ready-made solution in the form of a virtual sound card driver and found


After installation, this driver sends raw audio samples via udp to the address These samples are collected in a separate stream into a circular buffer.

In the SECAM standard, the audio carrier is offset from the video signal at 6.5 MHz and is transmitted with frequency modulation. To transmit both image and sound at the same time, you first need to modulate the audio signal, then simply add the samples of the video signal and the modulated audio signal:

Since the sampling frequency of the radio signal is much higher than that of the sound (in my case, the ratio turned out to be 312.5), you need to do resampling. I didn’t bother with interpolation, so a new sound sample is taken every 312.5 hackrf samples. Since the number is fractional, we had to build the simplest delay locked loop (if there are too few samples left in the audio buffer, then the resampling coefficient is 313, and if there are too many samples, the coefficient becomes 312).
If the audio driver does not send new packets, the buffer is emptied and the last sample from the buffer is fed to the modulator input.

All calculations of the sound signal are performed in fixed-point arithmetic, and the values ​​of trigonometric operations are obtained using a tabular method. Using float-point arithmetic and calculating sin and cos at runtime will waste too much CPU time. The table contains 2048 sine values ​​ranging from 0 to 2 Pi. It would be possible to store in the table only the range from 0 to Pi / 2, then the memory usage would decrease, but the algorithm would become more complicated. In the code, it looks like this:

Source codes

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);

        if (readAudioPosFrac > readAudioDivider)
            readAudioPosFrac = 0;
            if (bufferUsed-- > 0)
                // размер буфера равен 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;

The code is, as always, posted on the github

Similar Posts

Leave a Reply