DSP tutorial: SSB modulation using FFT

This example records audio and generates a lower sideband signal using FFT filtering, and saves it to a .wav file.

Single-sideband is one of the most popular modulation modes in analog voice radio communication today. SSB avoids the bandwidth doubling and unnecessary carrier emitting of amplitude modulation, thus concentrating radiated power only on the signal that carries the important information (for ex. voice).

If we take a 2KHz sine wave (modulator wave, this is the information we want to transport, so after demodulation, we would like to get this 2KHz beep) and modulate a 10KHz sine wave (carrier wave, this will carry our modulator wave) with it (you simply multiply the two sines), you get this:

You can see the two sidebands: LSB – lower sideband, the 8KHz sine, USB – upper sideband, the 12KHz sine. If you do this 2 sine multiplication in analog electronics, you will also get the 10KHz carrier wave.

Now this means if you want to transmit this 2KHz sine wave over radio on (for example) 3.791MHz, in theory you multiply the 2KHz sine with a 3.791MHz sine and transmit that resulting signal. In this case you will have to transmit both sidebands – if you transmit with 100 watts, your output power will distribute between the two sidebands, so this is not good, you only want to transmit one of the sidebands with full power.


A 4KHz wide LSB modulated voice signal

The first method is a very straight approach and only can be realized in software with no real benefit: let’s multiply our modulator wave with the carrier and then lowpass (or highpass) the sideband we want to keep.

This needs very sharp filtering. In the example I used FFT on the output, zeroed out all the unnecessary bins and then made an inverse FFT and got the result.

For demodulation of an SSB signal you can multiply it with a sine which has the frequency of the carrier wave (in this example 10KHz).

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
public class SSBFFT implements Runnable {
    private final static int SAMPLERATE = 44100;
    private final static int BUFFERSIZE = SAMPLERATE * 2;

    private TargetDataLine tdl;
    private DoubleFFT_1D fft;

    SSBFFT(TargetDataLine tdl) {
        this.tdl = tdl;
    }

    // converts float array to byte array
    private byte[] getBytesFromDoubles(final double[] audioData, final int storedSamples) {
        byte[] audioDataBytes = new byte[storedSamples * 2];

        for (int i = 0; i < storedSamples; i++) {
            // saturation
            audioData[i] = Math.min(1.0, Math.max(-1.0, audioData[i]));

            // scaling and conversion to integer
            int sample = (int) Math.round((audioData[i] + 1.0) * 32767.5) - 32768;

            byte high = (byte) ((sample >> 8) & 0xFF);
            byte low = (byte) (sample & 0xFF);
            audioDataBytes[i * 2] = low;
            audioDataBytes[i * 2 + 1] = high;
        }

        return audioDataBytes;
    }

    // saves the audio data given in audioDataBytes to a .wav file
    private void writeWavFile(final byte[] audioDataBytes, final int storedSamples, final String fileName) {
        AudioFormat audioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, SAMPLERATE, 16, 1, 2, SAMPLERATE, false);
        AudioInputStream audioInputStream = new AudioInputStream(new ByteArrayInputStream(audioDataBytes), audioFormat, storedSamples);

        try {
            FileOutputStream fileOutputStream = new FileOutputStream(fileName);
            AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, fileOutputStream);
            audioInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void lowPassFilter(double[] audioData, final int storedSamples, final double cutFreq) {
        // we need to initialize a buffer where we store our samples as complex numbers. first value is the real part, second is the imaginary.
        double[] fftData = new double[audioData.length * 2];
        for (int i = 0; i < storedSamples; i++) {
            // copying audio data to the fft data buffer, imaginary part is 0
            fftData[2 * i] = audioData[i];
            fftData[2 * i + 1] = 0;
        }

        // calculating the fft of the data, so we will have spectral power of each frequency component
        fft.complexForward(fftData);

        for (int i = 0; i < fftData.length; i += 2) {
            // lowpass
            if (i > ((cutFreq * (fftData.length/2)) / SAMPLERATE)*2)
                fftData[i] = fftData[i + 1] = 0;
        }
   
        // built-in scaling hangs the thread, so we don't use it
        fft.complexInverse(fftData, false);

        for (int i = 0; i < storedSamples; i++) {
            audioData[i] = fftData[2 * i] / (SAMPLERATE/4.0); // applying scaling
        }
    }

    @Override
    public void run() {
        byte[] abBuffer = new byte[tdl.getBufferSize()];
        double[] abBufferDouble = new double[abBuffer.length / 2];
        ByteArrayOutputStream baos = new ByteArrayOutputStream(); // this will store sound data
        double sinOscPhase = 0;
        double carrierFreq = 10000; // frequency of the sine wave

        fft = new DoubleFFT_1D(abBufferDouble.length);

        tdl.start();

        try {
            while (!Thread.interrupted()) {
                // waiting for the buffer to get filled
                while (tdl.available() < tdl.getBufferSize() * 0.5)
                    Thread.sleep(0, 1); // without this, the audio will be choppy

                int bytesRead = tdl.read(abBuffer, 0, tdl.available());

                // converting frames stored as bytes to double values
                int samplesRead = bytesRead / tdl.getFormat().getFrameSize();
                for (int i = 0; i < samplesRead; i++)
                    abBufferDouble[i] = ((abBuffer[i * 2] & 0xFF) | (abBuffer[i * 2 + 1] << 8)) / 32768.0;

                // generating a sine wave (carrier) and multiplying samples with it
                for (int samplePos = 0; samplePos < samplesRead; samplePos++) {
                    sinOscPhase += (2 * Math.PI * carrierFreq) / SAMPLERATE;
                    abBufferDouble[samplePos] *= Math.sin(sinOscPhase);

                    if (sinOscPhase >= 2 * Math.PI)
                        sinOscPhase -= 2 * Math.PI;
                }

                // zeroing out all data above the carrier frequency, so we get the low sideband
                lowPassFilter(abBufferDouble, samplesRead, carrierFreq);

                // demodulating (multiplying samples with the carrier again)
                /*sinOscPhase = 0;
                for (int samplePos = 0; samplePos < samplesRead; samplePos++) {
                    sinOscPhase += (2 * Math.PI * carrierFreq) / SAMPLERATE;
                    abBufferDouble[samplePos] *= Math.sin(sinOscPhase);

                    if (sinOscPhase >= 2 * Math.PI)
                        sinOscPhase -= 2 * Math.PI;
                }*/


                baos.write(getBytesFromDoubles(abBufferDouble, samplesRead), 0, samplesRead * 2);
            }
        } catch (InterruptedException e) {
        }

        tdl.stop();
        tdl.close();

        writeWavFile(baos.toByteArray(), baos.size() / 2, "output.wav");
    }

    public static void main(String[] args) {
        AudioFormat audioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, SAMPLERATE, 16, 1, 2, SAMPLERATE, false);
        DataLine.Info info = new DataLine.Info(TargetDataLine.class, audioFormat, BUFFERSIZE);

        TargetDataLine targetDataLine = null;
        try {
            targetDataLine = (TargetDataLine) AudioSystem.getLine(info);
            targetDataLine.open(audioFormat, BUFFERSIZE);
            System.out.println("Buffer size: " + targetDataLine.getBufferSize());
        } catch (LineUnavailableException e1) {
            e1.printStackTrace();
        }

        // creating the recorder thread from this class' instance
        SSBFFT ssbFFT = new SSBFFT(targetDataLine);
        Thread SSBFFTThread = new Thread(ssbFFT);

        // we use this to read line from the standard input
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        SSBFFTThread.setPriority(Thread.MAX_PRIORITY);
        SSBFFTThread.start();

        System.out.println("Recording... press ENTER to stop recording!");
        try {
            br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        SSBFFTThread.interrupt();

        try {
            // waiting for the recorder thread to stop
            SSBFFTThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Recording stopped.");
    }
}

download (850.5 kb)

VooDude 2012-12-13 19:03:56

This is the sort of thing I’m interested in. Most SDR are too fancy and too expensive. I’m interesting in building a hybrid DSP SDR/Analog transceiver that is both small in size and low in cost. SSB has to be part of it!

 
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