DSP tutorial: SSB modulation using FFT
Last modified: March 3rd, 2012This 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."); } } |
Trackback URL
1 Comment »
Trackback responses to this post
About me
I'm Nonoo. This is my blog about music, sounds, filmmaking, amateur radio, computers, programming, electronics and other things I'm obsessed with.
... »
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!