Работать с HackRF можно с помощью библиоткеки на языке Си. Программы типа SDR# и GNURadio используют именно ее. Чтобы начать передачу нужно подключиться к устройству и как минимум задать рабочую частоту и частоту дискретизации. После начала передачи периодически будет вызываться функция callback, в которой нужно заполнять буфер для передачи (или забирать из него данные если мы принимаем).
int hackrf_start_tx(hackrf_device* device, hackrf_sample_block_cb_fn callback, void* tx_ctx);
Для того чтобы передавать какие-то данные, эти данные должны существовать.
Наиболее простым решением будет использование буфера кадра, в котором лежат уже готовые семплы видеосигнала. Это позволяет максимально уменьшить время выполнения функции callback, т.к. если эта функция закончит свое выполнение после того как внутренний буфер hackRF опустошится, в передаваемом сигнале появятся артефакты.
Так как в телесигнале должны присутствовать синхроимпульсы, они тоже будут находиться в буфере кадров. Еще для того чтобы получить приемлемое разрешение по вертикали, нужно применять черезстрочную развертку. В итоге получилась примерно такая структура кадрового буфера:
На этом этапе уже можно выводить какие-нибудь статичные изображения, но это не так интересно. Немного погуглив, я обнаружил пример работы с драйвером виртуального дисплея github.com/LinJiabang/virtual-display
После изучения кода, понял, что функция LJB_VMON_PixelMain отправляет сообщения в UI поток после того как содержимое экрана меняется. Значит можно вызвать функцию наполнения буфера для hackRF в обработчике сообщения winapi WM_PAINT.
После переноса кода из этого проекта в основной и выполнения всех пунктов README получилось заставить винду задетектить виртуальный дисплей и передавать его содержимое в телевизор.
Вывод звука
Кроме того что телевизор умеет показывать изображения, он еще умеет и проигрывать звук.
Для этих целей я тоже поискал готовое решение в виде драйвера виртуальной звуковой карты и нашел scream.
Данный драйвер после установки отправляет по udp сырые семплы аудио на адрес 239.255.77.77:4010. Эти семплы собираются отдельным потоком в кольцевой буфер.
В стандарте SECAM несущая звука идет со смещением относительно видеосигнала на 6.5МГц и передается с частотной модуляцией. Чтобы передать одновременно и изображение и звук, сначала нужно промодулировать звуковой сигнал, затем просто сложить семплы видеосигнала и промодулированного звукового:
Так как частота семплирования радиосигнала намного больше чем у звука (в моем случае соотношение получилось 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 calcSinTable()
{
std::array result = std::array();
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 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