DSP tutorial: SSTV encoder

An SSTV transmission consists of the following parts:

  1. VOX tones (optional, but most SSTV apps use it)
  2. VIS code
  3. Image data
  4. FSK ID (optional)

The example encoder can produce the VOX tones, VIS code and image data, we don’t deal with the FSK ID here. If you want to use the FSK ID, here are the parameters: 6-bit bytes, LSB first, 45.45 baud, 1900 Hz = 1, 2100 Hz = 0, text data starts with 20 2A and ends in 01, add 0x20 and the data becomes ASCII.

VOX tones can be anything, I’ve heard musical tones playing as VOX tones several times on HF. :) Most of the time, these notes are played as SSTV VOX tones:

100 ms 1900 Hz
100 ms 1500 Hz
100 ms 1900 Hz
100 ms 1500 Hz
100 ms 2300 Hz
100 ms 1500 Hz
100 ms 2300 Hz
100 ms 1500 Hz

My encoder reads an image file, opens a window, plays the VOX tone, the VIS code and produces a Martin M1 SSTV signal. The sent image can be viewed in the previously opened window.

Resources

There’s a very good PDF for the VIS code and image data timings and format.

Here’s a good technical reference for SSTV.

Source code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
    private static void outputSineSample(double frequency) {
        oscPhase += (2 * Math.PI * frequency) / SAMPLERATE;
   
        double sample = Math.sin(oscPhase) * 0.2;
               
        if (oscPhase >= 2 * Math.PI)
            oscPhase -= 2 * Math.PI;

        outputToDevice(sample);
    }
   
    private static void outputToDevice(double sample) {
        outBuffer[outBufferPos++] = sample;

        if (outBufferPos == outBuffer.length) {
            sdl.write(getBytesFromDoubles(outBuffer, outBuffer.length), 0, outBuffer.length*2);
            wavOut.write(getBytesFromDoubles(outBuffer, outBuffer.length), 0, outBuffer.length*2);
            outBufferPos = 0;
        }
    }

    private static void flushDeviceBuffer() {
        sdl.write(getBytesFromDoubles(outBuffer, outBufferPos), 0, outBufferPos*2);
        wavOut.write(getBytesFromDoubles(outBuffer, outBufferPos), 0, outBufferPos*2);
        outBufferPos = 0;
    }
   
    private static void playTone(double frequency, double lengthInMSecs) {
        double samplesNeeded = SAMPLERATE*(lengthInMSecs/1000.0);
        for (double i = 0; i < samplesNeeded; i++)
            outputSineSample(frequency);
    }

    @Override
    public void run() {
        outBuffer = new double[BUFFERSIZE];

        //playTone(5000, 4576); outputToDeviceBuffer(); writeWavFile(wavOut.toByteArray(), wavOut.size() / 2, "output.wav"); System.exit(0);
        System.out.println("Playing VOX tones...");
        playTone(1900, 100);
        playTone(1500, 100);
        playTone(1900, 100);
        playTone(1500, 100);
        playTone(2300, 100);
        playTone(1500, 100);
        playTone(2300, 100);
        playTone(1500, 100);

        // VIS code
        System.out.print("Playing VIS (" + VIS + "): ");
        playTone(1900, 300); // leader tone
        playTone(1200, 10); // break
        playTone(1900, 300); // leader tone
        playTone(1200, 30); // VIS start bit
        int parityBit = 0;
        for (int i = 0; i < 7; i++) {
            int bit = (VIS >> i) & 1;
            playTone((bit == 1 ? 1100 : 1300), 30);
            System.out.print(bit);
            parityBit ^= bit;
        }
        System.out.print(" " + parityBit);
        playTone(parityBit == 1 ? 1100 : 1300, 30);
        playTone(1200, 30); // VIS stop bit
        System.out.println();

        // sending image
        double pixelLengthInS = 0.0004576;
        //double pixelLengthInS = 0.00045762; // qsstv uses this
        double syncLengthInS = 0.004862;
        double porchLengthInS = 0.000572;
        double separatorLengthInS = 0.000572;
        double channelLengthInS = pixelLengthInS*320;
        double channelGStartInS = syncLengthInS + porchLengthInS;
        double channelBStartInS = channelGStartInS + channelLengthInS + separatorLengthInS;
        double channelRStartInS = channelBStartInS + channelLengthInS + separatorLengthInS;
        double lineLengthInS = syncLengthInS + porchLengthInS + channelLengthInS + separatorLengthInS + channelLengthInS + separatorLengthInS + channelLengthInS + separatorLengthInS;
        double imageLengthInSamples = (lineLengthInS*256)*SAMPLERATE;
       
        double t, linet;

        for (int s = 0; s < imageLengthInSamples; s++) {
            t = s/(double)SAMPLERATE;
            linet = t % lineLengthInS;

            if (linet < syncLengthInS)
                outputSineSample(1200);
            if (linet >= syncLengthInS && linet < syncLengthInS + porchLengthInS)
                outputSineSample(1500);

            if (linet >= channelGStartInS && linet < channelGStartInS + channelLengthInS) {
                int y = (int)Math.floor(t/lineLengthInS);
                int x = (int)Math.floor(((linet-channelGStartInS)/channelLengthInS)*320);
                Color c = new Color(image.getRGB(x, y));
                outputSineSample(1500+c.getGreen()*3.1372549);
            }
            if (linet >= channelGStartInS + channelLengthInS && linet < channelBStartInS)
                outputSineSample(1500);
            if (linet >= channelBStartInS && linet < channelBStartInS + channelLengthInS) {
                int y = (int)Math.floor(t/lineLengthInS);
                int x = (int)Math.floor(((linet-channelBStartInS)/channelLengthInS)*320);
                Color c = new Color(image.getRGB(x, y));
                outputSineSample(1500+c.getBlue()*3.1372549);
            }
            if (linet >= channelBStartInS + channelLengthInS && linet < channelRStartInS)
                outputSineSample(1500);
            if (linet >= channelRStartInS && linet < channelRStartInS + channelLengthInS) {
                int y = (int)Math.floor(t/lineLengthInS);
                int x = (int)Math.floor(((linet-channelRStartInS)/channelLengthInS)*320);
                Color c = new Color(image.getRGB(x, y));
                outputSineSample(1500+c.getRed()*3.1372549);
                displayedImage[x][y] = c; repaint();
            }
            if (linet >= channelRStartInS + channelLengthInS)
                outputSineSample(1500);
        }

    }

download (mirror2) (198.2 kb, 137 dls, today: 0)

qu yi 2013-04-03 11:54:15

I cannot download the files of your DSP tutorial19,20 in your blog. Can you help me?I am interested in this program. I am learning it.

Nonoo 2013-04-03 12:03:27

I’ve corrected the links, sorry. Please try them now.

 
 
qu yi 2013-04-04 18:21:01

Thanks. I have download the whole java files in your DSP tutorial part. Some Files from No.12 to 23 cannot be unzip by using WinARA software under windowS xp. There is a error in unzipping operation. Why?

Nonoo 2013-04-04 19:06:57

Those are 7zip files, not zip files. Use 7zip and untar (or Total Commander with 7zip plugin).

 
 
Name (required)
E-mail (required - never shown publicly)
Webpage URL
Comment:
You may use <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> in your comment.

Trackback responses to this post

About me

Nonoo
I'm Nonoo. This is my blog about music, sounds, filmmaking, amateur radio, computers, programming, electronics and other things I'm obsessed with. ... »

Twitter

Listening now

My favorite artists