Привет! На дворе День радио, а значит у нас есть отличный повод сделать что-нибудь интересное. На днях мой взгляд упал на пылившийся в углу SDR-приемник, и тут понеслось.
Вообще spectrum painting, или рисование картинок на SDR-спектрограмме — развлечение довольно старое и нехитрое: берем картинку, прогоняем каждую строку через обратное преобразование Фурье и строку за строкой посылаем получившийся сигнал в эфир. Вот, например, готовый проект на гитхабе, который сразу генерирует выходной файл для HackRF или BladeRF. Но вот у меня, скажем, нет ни HackRF, ни чего-либо похожего, а передать картинку хочется, да как-нибудь попроще. Как же быть?
В принципе, можно было бы использовать простенький лабораторный генератор или DDS, но обычно их выходной буфер не превышает нескольких килобайт, в то время, как видео легко может занять десятки мегабайт. Постойте, но ведь у нас под рукой и так есть отличный девайс для воспроизведения длинных файлов — аудиовыход компьютера! Остается взять генератор несущей радиочастоты (в моем случае это 27 МГц) и промодулировать ее записью с компьютера на смесителе.
Смеситель (mixer) — это нелинейный элемент с двумя входами и одним выходом, который складывает или вычитает друг из друга входные частоты. Когда мы хотим послушать радио на 101.6 МГц на карманном приемнике, тот выставляет частоту гетеродина на 101.5 МГц и вычитает ее из несущей при помощи смесителя, понижая частоту до 100 кГц и завершая демодуляцию на ней. Мы будем делать то же самое, только в другую сторону: прибавим 27 МГц несущей к 10-20 кГц аудиосигнала, в котором закодирована картинка. На языке SDR-спектрограмм это означает, что картинка просто сместится вверх по частоте на 27 МГц.
Простейший смеситель можно собрать из диодов. Но мне было лень, и я взял какой-то готовый от MiniCircuits.
Белая коробочка — это и есть смеситель. Слева от него Red Pitaya в роли генератора несущей.
Тут начинаются нюансы. Первый заключается в том, что аудиокарта выдает сигнал только до 20 кГц, а промышленные смесители не оптимизированы для таких низких частот. Чтобы получить приемлемый сигнал на выходе, придется усилить сигналы на входе, что неизбежно приведет к нелинейностям — иными словами, генерации суммарных и разностных частот из самого аудиосигнала:
Это ограничивает рабочий диапазон от 10 до 20 кГц: выше 20 кГц не работает аудиокарта, ниже 10 кГц появляется разностный сигнал от аудиочастот (мы его еще увидим).
Второй нюанс заключается в том, что преобразование Фурье — комплексное, а в частотном, да и временном представлении сигнала существенную роль играют не только интенсивности, но и фазы. На практике чаще всего работают не с комплексными числами, а с IQ-представлением в двух ортогональных квадратурах, повернутых на 90 градусов. Например, в SDR-приемнике стоят два смесителя, которые сбивают сигнал с частотой гетеродина, повернутой на 90 градусов:
А в передатчике HackRF стоит микросхема MAX2839, которая сбивает I и Q компоненты сигнала с двумя ортогональными несущими на двух смесителях, суммирует их выходные сигналы и отправляет их дальше в антенный тракт:
В нашей поделке есть только один смеситель, а значит мы передаем сигнал только в одной квадратуре, то есть без учета фаз. Фурье-преобразование такого сигнала симметрично, а значит, помимо заветной картинки справа от несущей мы также увидим ее зеркальную реплику слева от несущей, и потеряем половину интенсивности. Это не страшно, но сигнал немного ослабит.
Переходим к вычислениям. Давайте передавать картинку стандартным битрейтом в 44100 сэмплов в секунду (профессор Котельников напоминает нам, что для 20 кГц больше и не понадобится), а одну строку картинки будем передавать за 2048 точек. Из-за вышеупомянутых нелинейных эффектов и зеркальной реплики мы можем использовать только четверть из них, но вообще лучше сделать картинку еще меньше. Пусть будет 256 пикселей.
Вот, в принципе, и все. Код очень похож на spectrum painter
import numpy as np
import scipy
import imageio.v2 as img
filename = 'img.png'
sr = 44100 # Hz
nfft = 2048
start_pix = nfft-256-1-256 # from which the image will be embedded
# Load the image
pic = np.float32(img.imread(filename)[:,:,0])
# Embed image starting from the given pixel
fftall = np.zeros((ffts.shape[0], nfft))
fftall[:, start_pix:(start_pix+pic.shape[1])] = pic
# Generate random phase vectors for the FFT bins
# This is important to prevent high peaks in the output
phases = 2*np.pi*np.random.rand(*fftall.shape)
rffts = fftall * np.exp(1j*phases)
# Perform the FFT per image line
ifft = np.fft.ifft(np.fft.ifftshift(rffts, axes=1), axis=1) / np.sqrt(float(nfft))
# Concatenate lines to form the final signal, take one quadrature, and normalize
signal = normalize(np.real(ifft.flatten()))
signal_norm = np.float32(signal / max(signal.max(), -signal.min()))
# Write the signal to a wav file
scipy.io.wavfile.write(f'out.wav', sr, signal_norm)
Ну что, пора запустить Doom? А вот и нет: Doom запускают программисты. Инженеры запускают Bad Apple!
Как я и говорил выше, реальный сигнал без мнимой компоненты дает зеркальное изображение относительно несущей, в точности как при амплитудной модуляции. Хотя оно оказалось заметно тусклее, чем ожидалось, вероятно, из-за нелинейностей. По той же причине появляется шум рядом с несущей (на разностях частот из картинки) и выше 22 кГц (на их второй гармонике).
Выглядит неплохо, но я здорово ускорил это видео. На самом деле передача одного кадра занимает почти 8 секунд! А можно ли сделать что-то более похожее на настоящее видео? Хотя бы пять кадров в секунду? Давайте считать.
Имеет ли смысл увеличивать sampling rate (SR) передатчика? Он определяет максимальную ширину спектра, в то время, как наша картинка занимает ширину Δf. При ширине картинки в W пикселей полная ширина спектра составит
После преобразования Фурье количество точек останется таким же, а значит, на передачу одной строчки изображения уйдет
Ага, то есть время на передачу одной строки вообще не зависит от sampling rate! А значит, увеличивать его никакого смысла нет. Время на передачу всего кадра размером W х H составит
Получается, для T = 0.2 с (5 кадров в секунду) и Δf = 20 — 10 кГц = 10 кГц один кадр не сможет превышать где-то 40 х 40 пикселей. В принципе с этим уже можно работать.
И, наконец, самый главный вопрос: а можно ли действительно смотреть видео на SDR, а не просто проматывать пленку? А почему бы и нет: например, можно вставить в спектр синхроимпульсы перед началом каждого кадра и написать простенький интерфейс для приемника, который будет их распознавать. А можно просто взять другую программу для работы с SDR. Например, SDRAngel перерисовывает спектрограмму заданое количество раз в секунду:
Остаeтся подобрать sampling rate так, чтобы один кадр занимал ровно одну пятую секунды, и чтобы видео не плыло вверх-вниз как на непрогретом ламповом телевизоре. Размер моего кадра составляет 51х32 пикселя, поэтому для преобразования Фурье хватит длины выборки длиной 256 (чуть больше 51х4). В этом случае sampling rate будет равен 256 x 32 x 5 = 40 960 сэмплов в секунду.
Ну что, попробуем?
С Днем Радио! Всем 73!
Автор: Альберт