## DSP tutorial: SSB modulation using the Weaver method

This 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:

 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 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.");     } }

download (973.6 kb)

## 12 Comments »

Walter 2013-07-20 17:05:26

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).

2013-07-21 09:07:03

Maybe the problem is that you commented out that DoubleFFT line.

Walter 2013-07-21 17:24:40

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…

Walter 2013-07-21 19:41:46

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…

Walter 2013-09-01 16:58:26

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…

2013-09-01 17:31:36

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. :)

SAM 2013-10-03 06:06:08

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

2013-10-03 08:43:54

Hello, this is not a plugin, and I don’t know what V machine is. You’re missing something.

SAM 2013-10-04 04:35:03

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

2013-10-04 09:46:52

Yes it’s possible, for example Sound Forge has a tool called “pitch shifter”. I think Audacity has this feature too.

SAM 2013-10-05 06:43:56

Thanks, I will give it a try. But I was wondering what does SSB modulation exactly do?

SAM

2013-10-05 08:45:41

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.

## 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. ... »