DSP tutorial: SSB modulation using the Weaver method
Last modified: March 3rd, 2012This example records audio and generates the lower sideband signal of it using the Weaver method, and saves it to a .wav file.
The Weaver method is the most complicated one, it uses two quadrature oscillators and lowpass filters. A very nicely illustrated paper on this method can be downloaded here, read this if you want to learn the mechanisms of this method.
For lowpass filtering I used a Butterworth filter. It’s cutFreq is 10kHz because it is not very steep, but in an ideal case, it should be samplerate/4. I left the lines for FFT filtering in the code (commented out).
Example resulting signal:
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 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | public class SSBWeaver implements Runnable { private final static int SAMPLERATE = 44100; private final static int BUFFERSIZE = SAMPLERATE * 2; private final static int IIR_NZEROS = 10; private final static int IIR_NPOLES = 10; private final static double IIR_GAIN = 3.452507086e+02; // these are used for the two lowpass filters private double xv_H[], yv_H[]; private double xv_L[], yv_L[]; private TargetDataLine tdl; //private DoubleFFT_1D fft; public SSBWeaver(TargetDataLine tdl) { this.tdl = tdl; } // IIR Butterworth lowpass filter 1 // see http://www-users.cs.york.ac.uk/~fisher/mkfilter/ private double lowPassFilterH(double in) { xv_H[0] = xv_H[1]; xv_H[1] = xv_H[2]; xv_H[2] = xv_H[3]; xv_H[3] = xv_H[4]; xv_H[4] = xv_H[5]; xv_H[5] = xv_H[6]; xv_H[6] = xv_H[7]; xv_H[7] = xv_H[8]; xv_H[8] = xv_H[9]; xv_H[9] = xv_H[10]; xv_H[10] = in / IIR_GAIN; yv_H[0] = yv_H[1]; yv_H[1] = yv_H[2]; yv_H[2] = yv_H[3]; yv_H[3] = yv_H[4]; yv_H[4] = yv_H[5]; yv_H[5] = yv_H[6]; yv_H[6] = yv_H[7]; yv_H[7] = yv_H[8]; yv_H[8] = yv_H[9]; yv_H[9] = yv_H[10]; yv_H[10] = (xv_H[0] + xv_H[10]) + 10 * (xv_H[1] + xv_H[9]) + 45 * (xv_H[2] + xv_H[8]) + 120 * (xv_H[3] + xv_H[7]) + 210 * (xv_H[4] + xv_H[6]) + 252 * xv_H[5] + (-0.0000357500 * yv_H[0]) + (0.0006094904 * yv_H[1]) + (-0.0061672448 * yv_H[2]) + (0.0298428117 * yv_H[3]) + (-0.1344215690 * yv_H[4]) + (0.3160380661 * yv_H[5]) + (-0.8283557825 * yv_H[6]) + (1.0214778432 * yv_H[7]) + (-1.7026748886 * yv_H[8]) + (0.9259874207 * yv_H[9]); return yv_H[10]; } // IIR Butterworth lowpass filter 2 private double lowPassFilterL(double in) { xv_L[0] = xv_L[1]; xv_L[1] = xv_L[2]; xv_L[2] = xv_L[3]; xv_L[3] = xv_L[4]; xv_L[4] = xv_L[5]; xv_L[5] = xv_L[6]; xv_L[6] = xv_L[7]; xv_L[7] = xv_L[8]; xv_L[8] = xv_L[9]; xv_L[9] = xv_L[10]; xv_L[10] = in / IIR_GAIN; yv_L[0] = yv_L[1]; yv_L[1] = yv_L[2]; yv_L[2] = yv_L[3]; yv_L[3] = yv_L[4]; yv_L[4] = yv_L[5]; yv_L[5] = yv_L[6]; yv_L[6] = yv_L[7]; yv_L[7] = yv_L[8]; yv_L[8] = yv_L[9]; yv_L[9] = yv_L[10]; yv_L[10] = (xv_L[0] + xv_L[10]) + 10 * (xv_L[1] + xv_L[9]) + 45 * (xv_L[2] + xv_L[8]) + 120 * (xv_L[3] + xv_L[7]) + 210 * (xv_L[4] + xv_L[6]) + 252 * xv_L[5] + (-0.0000357500 * yv_L[0]) + (0.0006094904 * yv_L[1]) + (-0.0061672448 * yv_L[2]) + (0.0298428117 * yv_L[3]) + (-0.1344215690 * yv_L[4]) + (0.3160380661 * yv_L[5]) + (-0.8283557825 * yv_L[6]) + (1.0214778432 * yv_L[7]) + (-1.7026748886 * yv_L[8]) + (0.9259874207 * yv_L[9]); return yv_L[10]; } /* private void lowPassFilterFFT(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 } } */ // 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(); } } @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 osc1Phase = 0, osc2Phase = 0; double carrierFreq = 10000; double osc1Freq = SAMPLERATE/4; double osc2Freq = osc1Freq - carrierFreq; // pitch shift is given by the difference between the two oscillators double demodPhase = 0; //fft = new DoubleFFT_1D(abBufferDouble.length); double[] lp_H = new double[abBufferDouble.length]; double[] lp_L = new double[abBufferDouble.length]; xv_H = new double[IIR_NZEROS + 1]; yv_H = new double[IIR_NPOLES + 1]; xv_L = new double[IIR_NZEROS + 1]; yv_L = new double[IIR_NPOLES + 1]; 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; for (int samplePos = 0; samplePos < samplesRead; samplePos++) { osc1Phase += (2 * Math.PI * osc1Freq) / SAMPLERATE; lp_H[samplePos] = abBufferDouble[samplePos] * Math.sin(osc1Phase); lp_L[samplePos] = abBufferDouble[samplePos] * Math.cos(osc1Phase); if (osc1Phase >= 2 * Math.PI) osc1Phase -= 2 * Math.PI; } // filtering can be done using FFT also //lowPassFilterFFT(l1, samplesRead, osc1Freq); //lowPassFilterFFT(l2, samplesRead, osc1Freq); for (int samplePos = 0; samplePos < samplesRead; samplePos++) { lp_H[samplePos] = lowPassFilterH(lp_H[samplePos]); lp_L[samplePos] = lowPassFilterL(lp_L[samplePos]); } // compensating the delay of the IIR filter by delaying osc2 for (int i = 0; i < IIR_NZEROS; i++) { osc2Phase += (2 * Math.PI * osc2Freq) / SAMPLERATE; if (osc2Phase >= 2 * Math.PI) osc2Phase -= 2 * Math.PI; } for (int samplePos = 0; samplePos < samplesRead; samplePos++) { osc2Phase += (2 * Math.PI * osc2Freq) / SAMPLERATE; lp_H[samplePos] *= Math.sin(osc2Phase); lp_L[samplePos] *= Math.cos(osc2Phase); abBufferDouble[samplePos] = lp_H[samplePos] + lp_L[samplePos]; if (osc2Phase >= 2 * Math.PI) osc2Phase -= 2 * Math.PI; } // demodulating (multiplying samples with the carrier again) /*for (int samplePos = 0; samplePos < samplesRead; samplePos++) { demodPhase += (2 * Math.PI * carrierFreq) / SAMPLERATE; abBufferDouble[samplePos] *= Math.sin(demodPhase); if (demodPhase >= 2 * Math.PI) demodPhase -= 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 SSBWeaver ssbWeaver = new SSBWeaver(targetDataLine); Thread ssbWeaverThread = new Thread(ssbWeaver); // we use this to read line from the standard input BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); ssbWeaverThread.setPriority(Thread.MAX_PRIORITY); ssbWeaverThread.start(); System.out.println("Recording... press ENTER to stop recording!"); try { br.readLine(); } catch (IOException e) { e.printStackTrace(); } ssbWeaverThread.interrupt(); try { // waiting for the recorder thread to stop ssbWeaverThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Recording stopped."); } } |
Trackback URL
12 Comments »
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.
... »
Hi Nonoo,
I’ve been playing with your SSBWeaver.java program. I downloaded it and compiled it (after commenting out: import edu.emory.mathcs.jtransforms.fft.DoubleFFT_ID:). Then I tried recording some piano notes (e.g., A3, C4, etc.). When I played the .wav file, I expected to hear a pitch shift (along with some SSB distortion). But there was no pitch shift. I also tried changing the carrierFreq value from 10000 to 440. Still no pitch shift. Do you have any idea what I am doing wrong, or am I expecting the wrong response? Thanks in advance… I’m working on a software defined radio (SDR) project and wanted to add SSB reception. The current project is on SourceForge.net (enter: fun2audio in its Search window).
Maybe the problem is that you commented out that DoubleFFT line.
Hi again,
Since all of the other fft code had been commented out, I assumed that that line was not needed.
I will keep looking…
OK, now the results make sense! I used the Audacity audio program to run the output.wav file. I used its Analyze | Plot Spectrun… tool to look at its spectrum. For an input tone of 261.63 (middle C on a piano(,the output peaks at about 9000 Hz, which is well above my hearing range! Impressive code! Thanks for sharing it…
Hi again Nonoo
I ran your filter design at:
http://www-users.cs.york.ac.uk/~fisher/mkfilter/ with the same parameters as I assume you used: Butterworth, Lowpass, order=10, samplerate=44100, corner=10000. Everything was the same except the GAIN that was generated was: 7.432679795e+02. in your code the IIR_GAIN is: 3.452507086e+02. Do you remember where the difference came from? I’m trying to re-implement your program in C.
Thanks in advance…
Huh I can’t remember, but if you’re finished with the C conversion, please upload it somewhere and post the link to it here. :)
Hello,
I just downloaded a plugin for Frequency shifter for V Machine but I don’t have a V Machine physically. I just downloaded VFX software and installed on my Win XP and also the plugin Frequency Shifter (32 bit) was added.
I can be able to see the VST editor and all that stuff at the right panel side of VFX application. In the options I have configured the DirectSound under Audio system option.
My question is I’m trying to change (shift) the FREQUENCY of a normal audio file .wav or .mp3 with 44100 khz sample rate with 8 or 16 bit per sample 128 or 320 kbps file from its normal frequency range, say – 8 – 10 khz and shift it to the range between 18- 20 KHZ. Is this possible with the above setup or do I require a V Machine? If it is possible to do it without V Machine then I would love to hear it from you using SSB modulation etc., and I will be obliged.
The statistics are given below –
Wavosaur statistics and information
————————————
Statistics:
RMS power L: 7.28% (-22.75 dB)
RMS power R: 7.27% (-22.76 dB)
Average value L (DC offset): -0.00% (-94.74 dB)
Average value R (DC offset): 0.00% (-89.95 dB)
Min value L: -98.11% (-0.17 dB)
Min value R: -100.13% (0.01 dB)
Max value L: 91.92% (-0.73 dB)
Max value R: 89.03% (-1.01 dB)
General information:
Sample name: D:\Ex\sam.mp3
Channel number: 2
Sample number: 1166976
Total duration: 00:00:26:462
Frequency: 44100Hz
Bits per sample: 16
Format: PCM
Selection:
Selection start: 0
Selection end: 1166975
Delta: 1166976
No loop points
No marker points
No automation points
Thanks in advance for your valuable help.
Warm Regards,
SAM
Hello, this is not a plugin, and I don’t know what V machine is. You’re missing something.
Ok,
Let me make it simple.
I have an audio file whose specifications and details are given in my previous post here.
My sole purpose is to know if it is possible to change (SHIFT) the frequency of a file like above from 8-10 KHZ (for instance) to 18-20 KHZ and REVERSE of that. For instance a high frequency file of 18-20 KHZ and shift its frequency to 8-10 KHZ for whatever normal human hearing range is?
Is this possible using any wave editor program like Audacity or any other application in Win XP?
Thanks in advance for your valuable help.
SAM
Yes it’s possible, for example Sound Forge has a tool called “pitch shifter”. I think Audacity has this feature too.
Thanks, I will give it a try. But I was wondering what does SSB modulation exactly do?
SAM
http://en.wikipedia.org/wiki/Single-sideband_modulation