# Panorama FM, or How to See all FM Stations Using SDR

Probably everyone, who is at least a bit interested in radio and communications, knows that using an SDR receiver it is possible to receive and process simultaneously a wide band of the radio spectrum. Displaying the waterfall in such programs as HDSDR or SDR# is not surprising. I will show how to build a pseudo-3D spectrum of the FM frequency band using RTL-SDR, GNU Radio, and about 100 lines of Python code.

We will also take a more powerful receiver and will look at the entire FM band 88–108MHz.

Technically, the task is quite simple. The SDR receiver digitizes the incoming radio signal using a reasonably fast ADC. At the output, we get a broadband IQ signal in the form of an array of numbers coming from the ADC with a bandwidth corresponding to the ADC sample rate. The ADC frequency determines the maximum bandwidth, that can be used. It is the same process as in a PC sound card, we only have not 22.050, but 2.000.000 or even 10.000.000 samples per second. To display the radio spectrum on the screen, we must perform the Fast Fourier Transform on the data array, it converts the data from the so-called «time domain» to the «frequency domain». Then we show the data on the screen, and the problem solved. I will also try to use the minimum of code, so GNU Radio can help us with data processing.

For tests, we first need an RTL-SDR receiver, the price of which is about \$35. It allows us to receive radio signals in the 70..1700 MHz frequency range using a bandwidth of up to 2 MHz:

If anyone wants to do the tests using RTL-SDR, it’s recommended to get the receiver like on the photo. There are cheaper clones available, but they are of worse quality.

Well, let’s get started.

First, we need to get and process data from the receiver. The GNU Radio connection graph is shown in the figure:

As we can see, we get data from the receiver, convert the continuous datastream into a set of “vectors” of 1024 values size, perform FFT on these vectors, convert values ​​from complex to real, and finally send data via UDP. Of course, all this could be done in a pure Python using SoapySDR and numpy libraries, but the amount of code would be somewhat larger.

The QT GUI Frequency Sink block is needed only for «debugging», using this block, we can make sure that the radio stations are visible and, if necessary, adjust the receiver gain. While the application is running, the picture should look something like this:

If everything works, the Frequency Sink block can be disabled, also in the GNU Radio project settings, we can optionally specify «No GUI» mode and not waste UI resources. In principle, this program can be running as a service without any UI.

# Rendering

Since we transmit data via UDP, we can receive it with any client, and even on another PC. I will use Python, it is quite enough for the prototype.

First, let’s get the UDP data:

`fft_size = 1024udp_data = NoneUDP_IP = "127.0.0.1"UDP_PORT = 40868sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDPsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind((UDP_IP, UDP_PORT))sock.settimeout(0.5)try:    data, addr = sock.recvfrom(fft_size * 4)    if len(data) == 4096:        udp_data = np.frombuffer(data, dtype=np.float32)        return Trueexcept socket.timeout:    pass`

Because we work with graphics, it is convenient to use the pygame library. Drawing a 3D spectrum is simple, we store the data in an array, and draw lines from top to bottom, from older to newer.

`fft_size = 1024depth = 255fft_data = np.zeros([depth, fft_size])def draw_image(screen, font):    x_left, x_right, y_bottom = 0, img_size,  img_size - 5    # Draw spectrum in pseudo-3d    for d in reversed(range(depth)):        for x in range(fft_size - 1):            d_x1, d_x2, d_y1, d_y2 = x + d, x + d + 1, y_bottom - int(y_ampl*fft_data[d][x]) - y_shift - d, y_bottom - int(y_ampl*fft_data[d][x+1]) - y_shift - d            if d_y1 > y_bottom - 34: d_y1 = y_bottom - 34            if d_y2 > y_bottom - 34: d_y2 = y_bottom - 34            dim = 1 - 0.8*(d/depth)            color = int(dim*data_2_color(fft_data[d][x]))            pygame.draw.line(screen, (color//2,color,0) if d > 0 else (0, 250, 0), (d_x1, d_y1), (d_x2, d_y2), (2 if d == 0 else 1))`

We can also display the frequencies and station names on the screen. The Fourier Transform algorithm gives an output of 1024 points corresponding to the bandwidth of the receiver. We know the center frequency, so calculating a pixel position can be done using an elementary school formula.

`stations = [("101.8 FM", 101.8), ("Rock FM", 102.4), ...]for st_name, freq in stations:    x_pos = fft_size*(freq - center_freq)*1000000//sample_rate    textsurface = font.render(st_name, False, (255, 255, 0))    screen.blit(textsurface, (img_size//2 + x_pos - textsurface.get_width()//2, y_bottom - 22))`

Actually that’s it, we can run both programs at the same time, and on the screen, we get a panorama showing the currently working FM stations in real-time:

It is easy to see that different stations have varying signal levels and we can even distinguish mono and stereo broadcasting.

Well, now I will show the promised panorama of the entire FM band. To do this, we have to put the RTL-SDR aside and use a better radio. For example, like this:

I use a professional grade Ettus Research SDR, but from the code perspective, everything is the same, it’s just needed to change one block to another in GNU Radio. And so it looks on the spectrum with a reception bandwidth of 24 MHz:

It is interesting to see the diversity of signal strength from different FM stations.

Of course, it is possible to receive not only FM stations but also any others within the operating frequencies of the SDR. For example, this is how the airband looks like:

We can see some permanently operating frequencies (probably the ATIS weather service) and intermittent radio communications between ground and pilots. And this is how the GSM-band spectrum looks like (the GSM-signal is wider than 24 MHz and does not fit completely):

# Conclusion

As we can see, the study of radio spectrum is quite exciting, especially in 3D. Of course, there was no purpose here of making «another spectrum analyzer», this is just a prototype made for fun. Alas, rendering is slow, Python is not the best choice for displaying several thousand primitives on screen. Lines colouring algorithm also can be improved.

As usual, I wish all the readers successful experiments.

The full rendering source code is attached below.

`import numpy as np from matplotlib import pyplot as plt from PIL import Image, ImageDraw import sys import pygame from pygame.locals import * from threading import Thread import io import cv2 import time import socket# FFTreceiver_name = "RTL-SDR"center_freq = 102.5sample_rate = 1800000stations = [("101.8", 101.8), ("102.1", 102.1), ("102.4", 102.4), ("102.7", 102.7), ("103.0", 103.0), ("103.2", 103.2)]# Load data from UDPUDP_IP = "127.0.0.1"UDP_PORT = 40868udp_data = Nonesock = None# Panorama historyfft_size = 1024depth = 255fft_data = np.zeros([depth, fft_size])# Canvas and drawimg_size = (fft_size, fft_size*9//16)y_ampl = 90color_ampl = 70y_shift = 250def udp_prepare():    global sock    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)    sock.bind((UDP_IP, UDP_PORT))    sock.settimeout(0.5)def udp_getdata():    global sock, udp_data    try:        data, addr = sock.recvfrom(fft_size * 4)        if len(data) == 4096:           udp_data = np.frombuffer(data, dtype=np.float32)           return True    except socket.timeout:        pass    return Falsedef clear_data():    for y in range(depth):        fft_data[y, :] = np.full((fft_size,), -1024)def add_new_line():    global udp_data, fft_data    # Shift old data up    for y in reversed(range(depth - 1)):        fft_data[y + 1, :] = fft_data[y, :]    # Put new data at the bottom line    if udp_data is not None:        fft_data[0, :] = udp_datadef data_2_color(data):    c = -data + 2  # TODO: detect noise floor of the spectrum    color = 150 - int(color_ampl * c)    if color < 20:        color = 20    if color > 150:       color = 150    return colordef draw_image(screen, font):    x_left, x_right, y_bottom = 0, img_size, img_size - 5    # Draw spectrum in pseudo-3d    for d in reversed(range(depth)):      for x in range(fft_size - 1):         d_x1, d_x2, d_y1, d_y2 = x + d, x + d + 1, y_bottom - int(y_ampl*fft_data[d][x]) - y_shift - d, y_bottom - int(y_ampl*fft_data[d][x+1]) - y_shift - d         if d_y1 > y_bottom - 34: d_y1 = y_bottom - 34         if d_y2 > y_bottom - 34: d_y2 = y_bottom - 34         dim = 1 - 0.8*(d/depth)         color = int(dim*data_2_color(fft_data[d][x]))         pygame.draw.line(screen, (color//2,color,0) if d > 0 else (0, 250, 0), (d_x1, d_y1), (d_x2, d_y2), (2 if d == 0 else 1))    # Bottom line    pygame.draw.line(screen, (0,100,0), (x_left, y_bottom - 30), (x_right, y_bottom - 30), 2)    # Station names    for st_name, freq in stations:        x_pos = fft_size*(freq - center_freq)*1000000//sample_rate        textsurface = font.render(st_name, False, (255, 255, 0))        screen.blit(textsurface, (img_size//2 + x_pos - textsurface.get_width()//2, y_bottom - 22))    text_mhz = font.render("MHz", False, (255, 255, 0))    screen.blit(text_mhz, (img_size - 5 - text_mhz.get_width(), y_bottom - 22))if __name__ == "__main__":    # UI init    screen = pygame.display.set_mode(img_size)    pygame.display.set_caption(receiver_name)    pygame.font.init()    font = pygame.font.SysFont('Arial Bold', 30)    # Subscribe to UDP    clear_data()    udp_prepare()    # Main loop    is_active = True    while is_active:        # Get new data        if udp_getdata():            add_new_line()            # Update screen            screen.fill((0, 0, 0))            draw_image(screen, font)            pygame.display.flip()        # Check sys events        for events in pygame.event.get():            if events.type == QUIT:                is_active = False`

## Dev Genius

### By Dev Genius

The best stories sent monthly to your email. Take a look

Written by

Written by

## Dmitrii Eliuseev

#### Python and IoT Developer, science and ham radio enthusiast 