SWHarden.com

The personal website of Scott W Harden

vdFSK Modulation

My goal is to create a QRPP (extremely low power) transmitter and modulation method to send QRSS (extremely slow, frequency shifting data) efficiently, able to be decoded visually or with automated image analysis software. This evolving post will document the thought process and development behind AJ4VD's Frequency Shift Keying method, vdFSK.

Briefly, this is what my idea is. Rather than standard 2-frequencies (low for space, high for tone) QRSS3 (3 seconds per dot), I eliminate the need for pauses between dots by using 3 frequencies (low for a space between letters, medium for dot, high for dash). The following images compare my call sign (AJ4VD) being sent with the old method, and the vdFSK method.

Again, both of these images say the same thing: AJ4VD, .- .--- ....- ...- -.. However, note that the above image has greater than a 3 second dot, so it’s unfairly long if you look at the time scale. Until I get a more fairly representative image, just appreciate it graphically. It’s obviously faster to send 3 frequencies rather than two. In my case, it’s over 200% faster.

This is the code to generate audio files converting a string of text into vdFSK audio, saving the output as a WAV file. Spectrographs can be created from these WAV files.

generate_audio.py

# converts a string into vdFSK audio saved as a WAV file

import numpy
import wave
from morse import *


def makeTone(freq, duration=1, samplerate=5000, shape=True):
    signal = numpy.arange(duration*samplerate) / \
        float(samplerate)*float(freq)*3.14*2
    signal = numpy.sin(signal)*16384
    if shape == True:  # soften edges
        for i in range(100):
            signal[i] = signal[i]*(i/100.0)
            signal[-i] = signal[-i]*(i/100.0)
    ssignal = ''
    for i in range(len(signal)):  # make it binary
        ssignal += wave.struct.pack('h', signal[i])
    return ssignal


def text2tone(msg, base=800, sep=5):
    audio = ''
    mult = 3  # secs per beep
    msg = " "+msg+" "
    for char in msg.lower():
        morse = lookup[char]
        print char, morse
        audio += makeTone(base, mult)
        for step in lookup[char]:
            if step[0] == ".":
                audio += makeTone(base+sep, int(step[1])*mult)
            if step[0] == "-":
                audio += makeTone(base+sep*2, int(step[1])*mult)
            if step[0] == "|":
                audio += makeTone(base, 3*mult)
    return audio


msg = "aj4vd"
file = wave.open('test.wav', 'wb')
file.setparams((1, 2, 5000, 5000*4, 'NONE', 'noncompressed'))
file.writeframes(text2tone(msg))
file.close()

print 'file written'

morse.py

# library for converting between text and Morse code
raw_lookup="""
a.- b-... c-.-. d-.. e. f..-. g--. h.... i.. j.--- k-- l.-.. m--
n-. o--- p.--. q--.- r.-. s... t- u.- v...- w.-- x-..- y-.-- z--..
0----- 1.---- 2..--- 3...-- 4....- 5..... 6-.... 7--... 8---.. 9----.
..-.-.- =-...- :---... ,--..-- /-..-. --....-
""".replace("n","").split(" ")

lookup={}
lookup[" "]=["|1"]
for char in raw_lookup:
    """This is a silly way to do it, but it works."""
    char,code=char[0],char[1:]
    code=code.replace("-----","x15 ")
    code=code.replace("----","x14 ")
    code=code.replace("---","x13 ")
    code=code.replace("--","x12 ")
    code=code.replace("-","x11 ")
    code=code.replace(".....","x05 ")
    code=code.replace("....","x04 ")
    code=code.replace("...","x03 ")
    code=code.replace("..","x02 ")
    code=code.replace(".","x01 ")
    code=code.replace("x0",'.')
    code=code.replace("x1",'-')
    code=code.split(" ")[:-1]
    #print char,code
    lookup[char]=code

Automated decoding is trivial. The image above was analyzed, turned into the image below, and the string (AJ4VD) was extracted:

decode.py

# given an image, it finds peaks and pulls data out
from PIL import Image
from PIL import ImageDraw
import pylab
import numpy

pixelSeek = 10
pixelShift = 15


def findPeak(data):
    maxVal = 0
    maxX = 0
    for x in range(len(data)):
        if data[x] > maxVal:
            maxVal, maxX = data[x], x
    return maxX


def peaks2morse(peaks):
    baseFreq = peaks[0]
    lastSignal = peaks[0]
    lastChange = 0
    directions = []
    for i in range(len(peaks)):
        if abs(peaks[i]-baseFreq) < pixelSeek:
            baseFreq = peaks[i]
        if abs(peaks[i]-lastSignal) < pixelSeek and i < len(peaks)-1:
            lastChange += 1
        else:
            if abs(baseFreq-lastSignal) < pixelSeek:
                c = " "
            if abs(baseFreq-lastSignal) < pixelSeek:
                c = " "
            if abs(baseFreq-lastSignal) < pixelSeek:
                c = " "
            directions.append(
                [lastSignal, lastChange, baseFreq, baseFreq-lastSignal])
            lastChange = 0
        lastSignal = peaks[i]
    return directions


def morse2image(directions):
    im = Image.new("L", (300, 100), 0)
    draw = ImageDraw.Draw(im)
    lastx = 0
    for d in directions:
        print d
        draw.line((lastx, d[0], lastx+d[1], d[0]), width=5, fill=255)
        lastx = lastx+d[1]
    im.show()


im = Image.open('raw.png')
pix = im.load()
data = numpy.zeros(im.size)
for x in range(im.size[0]):
    for y in range(im.size[1]):
        data[x][y] = pix[x, y]

peaks = []
for i in range(im.size[0]):
    peaks.append(findPeak(data[i]))

morse = peaks2morse(peaks)
morse2image(morse)
print morse