I had been attending the HAM course for a month when I saw SSTV for the first time, and I really liked the idea of transmitting images over low bandwidth channels. I tried several solutions including QSSTV for desktop and DroidSSTV for mobile usage, but found slowrx to be the best of all, but it was receive-only. I even contributed a patch to make it usable on machines with more than one sound card (think HDMI), and started thinking about developing a transmit-only counterpart.
Back in the university days, vmiklos gave me the idea of implementing non-trivial tasks in Python (such as solving Sudoku puzzles in Erlang and Prolog), so I started PySSTV on a day I had time and limited network connectivity. I relied heavily on the great SSTV book and testing with slowrx. For the purposes of latter, I used the ALSA loopback device that made it possible to interconnect an application playing sound with another that records it. Below is the result of such a test with event my call sign sent in FSK being recognized at the bottom. (I used the OE prefix since it was Stadtflucht6 – thankfully, I could use the MetaFunk antenna to test the rig, although as it turned out, Austrians don't use that much SSTV as no-one replied.)
My idea was to create a simple (preferably pure Python) implementation that
helped me understand how SSTV works. Although later I performed optimizations,
the basic design remained the same, as outlined below. The implementation
relies heavily on Python generators so if you're not familiar with things
yield statement, I advise you to read into it first.
Phase 1 of 3: encoding images as an input to the FM modulator
As SSTV images are effectively modulated using FM, the first or innermost
phase reads the input image and produces input to the FM modulator in the form
of frequency-duration pairs. As the standard references milliseconds, duration
float in ms, and since SSTV operates on voice frequencies, frequency
is also an
float in Hz. As Python provides powerful immutable tuples, I used
them to tie these values together. The
gen_freq_bits method of the
class implements this and generates such tuples when called.
SSTV is a generic class located in the sstv module, and provides a
frame for common functionality, such as emitting any headers and trailers. It
calls methods (
gen_image_tuples) and reads attributes (
VIS_CODE) that can
be overridden / set by descendant classes such as
Images are read using PIL objects, so the image can be loaded using
simple PIL methods and/or generated/modified using Python code.
Phase 2 of 3: FM modulation and sampling
gen_values method of the
SSTV class iterates over the values returned
gen_freq_bits and implements a simple FM modulator that generates a fixed
sine wave of fixed amplitude. It's also a generator that yields
between -1 and +1, the number of those samples per seconds is determined by
samples_per_sec attribute, usually set upon initialization.
Phase 3 of 3: quantization
Although later I found that floats can also be used in WAVE (
.wav) files, I
wasn't aware of it earlier, so I implemented a method called
performs quantization by iterating over the output of
int values this time. I used quantization noise using additive noise,
which introduced a little bit of randomness by the output, which was
compensated in the test suite by using assertAlmostEqual with a
value of 1.
Optimization and examples
Although it was meant to be a proof of concept, it turned out to be quite usable
on its own. So I started profiling it, and managed to make it run so fast that
now most of the time is taken by the overhead of the generators; it turns out
yield means the cost of a function call. For example, I realized
that generating a random value per sample is slow, and the quality of the output
remains the same if I generate 1024 random values and use itertools.cycle
to repeat them as long as there's input data.
In the end, performance was quite good on my desktop, but resulted in long runs
on Raspberry Pi (more about that later). So I created two simple tools that
made the output of the first two phases above accessible on the standard output.
As I mentioned above, every
yield was expensive at this stage of optimization,
and phase 2 and 3 used the largest amount of it (one per pixel vs. one per
sample). On the other hand, these two phases were the simplest ones, so I
reimplemented them using C in UNIXSSTV, so gen_freq_bits.py
can be used to get the best of both worlds.
I also created two examples to show the power of extensibility Python provides
in such few lines of code. The
examples module/directory contains scripts for
- playing audio directly using PyAudio,
- laying a text over the image using PIL calls, and
- using inotify with the pyinotify bindings to implement a simple repeater.
Reception, contribution and real-world usage
After having a working version, I sent e-mails to some mailing lists and got quite a few replies. First, some people measured that it took only 240 lines to implement a few modes, and I was surprised by this. HA5CBM told me about his idea of putting a small computer and camera into a CCTV case, attaching it to an UHF radio, transmitting live imagery on a regular basis. I liked the idea and bought a Raspberry Pi, which can generate a Martin M2 modulated WAVE file using UNIXSSTV in 30 seconds. Documentation and photos can be found on the H.A.C.K. project page, source code is available in a GitHub repo.
Contribution came from another direction, Joël Franusic submitted a pull request called Minor updates which improved some things in the code and raised my motivation. In the end, he created a dial-a-cat service and posted a great write-up on the Twilio blog.