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
like the 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
is an 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 SSTV
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 Robot8BW
or MartinM1
.
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
The gen_values
method of the SSTV
class iterates over the values returned
by 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 float
values
between -1 and +1, the number of those samples per seconds is determined by
the 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 gen_samples
that
performs quantization by iterating over the output of gen_values
, yielding
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 delta
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
that every 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.
If you do something like this, I'd be glad to hear about it, the source code is available under MIT license in my GitHub repository and on PyPI.