Play rtl_tcp radio on iPad using Pythonista

For the longest time, I have wanted to listen to the radio on my iPad, streaming from my dvb-t dongle. The dvb-t stick ("TV tuner") was primarily intended to play broadcast TV, but also has the capability of outputing IQ samples from its ADC, so that the data can be processed play audio. In short, you can use the dvb-t stick as a cheap software defined radio (sdr). The prerequisite is that you have to install some special drivers, block the default ones (i.e., blacklist them), and install the rtl_sdr package.

The dvb-t dongle is a nifty little USB device to which you can connect an antenna (through a few RF adapters). Now obviously you can't just connect this USB device directly to the iPad with ease; however, along with the rtl_sdr package comes the rtl_tcp utility. This utility can run the TV tuner, and send the IQ samples over a TCP connection. You can also send commands over the TCP connection, real time, to modify the frequency, RF gain, etc.; of the TV tuner.

Currently, I have an odroid U3 running Arch Linux. I have installed the rtl_sdr package, and also the necessary drivers. My goal is to stream IQ samples over the network to my iPad, and then convert those IQ samples to audio. I could write an app for my iPad to do this, but alas, I do not have a Mac. I do, however, have the app Pythonista installed on my iPad. This is a pretty good implementation of python on iOS, so it should be able to do all this processing.

I hope to document all of my steps here, and (time permitting) come up with an awesome SDR, written in Pythonista. So far, I have figured out how to:

  1. Write samples to file, and convert them to actual numbers,
  2. Normalize the numbers appropriately. See rtlsdr.py project

Here is the code I made to accomplish this. I referred to the above link for the normalization portion of the code.

#!/usr/local/bin/python

import struct
from os.path import getsize as getsize
from math    import floor   as floor
# rtl sdr tuner outputs unsigned integers.  There are no headers
# or any formatting to the data; it is just raw samples.
# They must be normalized, and put between -1 and 1.  This snippet
# only covers converting from a byte written in from the raw output
# to an integer, and the normalization.

# First, created 'sample.bin' file as follows (at Shell prompt):
# rtl_sdr ./sample.bin -s 262144 -f 102.5e6
# Let it run for a few seconds, got a ~3.6MB file!

# Get number of bytes in file.  There are two bytes per sample:
# one byte for an I, and one byte for a Q.
num_bytes = getsize('sample.bin')
num_samples = int(floor(num_bytes/2))

# Open file in byte-mode, read all samples into list.
# Normalize list to values between -1 and 1.
fl = open('sample.bin', 'rb')
iq_list = []
for i in range(0,num_samples):
  ibyte = fl.read(1)  # Read in one Byte; assume it's I.
  qbyte = fl.read(1)  # Read in one Byte: assume it's Q.
  ivalue = struct.unpack('B', ibyte)[0] # Convert byte to integer.
  qvalue = struct.unpack('B', qbyte)[0] # Convert byte to integer.
  iq_list.append([ivalue/(255/2.0)-1, qvalue/(255/2.0)-1]) # Normalize.
#

# Print out the first 10 samples as a check.
for i in range(0,10):
  print(iq_list[i])

Comments !