FM modulation

FM (Frequency Modulation) is the mainstream technology employed in hi-fi analog radio. In this post, I will try to show how a signal "sounds" once it is modulated this way, and of course show a simple implementation in Python.

We have this original speech of mine as input signal — the same I used for AM modulation:

Now I modulate it to FM, using a carrier of 5512.5 Hz (which happens to be 44100/8) and modulation band of +/-1000Hz. Please don't hear it in high volume, otherwise it will blow your head!

Impossible as it may seem, the voice can be recovered from the modulated version:

Now, let's see how the sausage is stuffed. First, a bit of background explanations.

FM changes the frequency of a central carrier, based on input signal strength. Stronger signals will cause farther deviations in modulation, regardless of input signal frequency. The carrier output is always the same power. In order to limit the minimum and maximum carrier frequencies, the strength of input signal must be bounded.

This is completely different from AM, where the carrier power oscilates to mimick the input signal, but the AM carrier frequency is fixed.

FM is much more robust against noise, because the carrier output is always at maximum power, while AM output may be overwhelmed by noise when it is weaker. In the other hand, FM needs more bandwidth than AM to transmit the same signal.

Here is the FM modulator written in Python, very simple indeed:

#!/usr/bin/env python import wave, struct, math baseband = wave.open("paralelepipedo_lopass.wav", "r") FM_BAND = 1000.0 FM_CARRIER = 44100 / 8.0 fm = wave.open("fm.wav", "w") fm.setnchannels(1) fm.setsampwidth(2) fm.setframerate(44100) integ_base = 0 for n in range(0, baseband.getnframes()): base = struct.unpack('h', baseband.readframes(1))[0] / 32768.0 # Base signal is integrated (not mandatory, but improves # volume at demodulation side) integ_base += base # The FM trick: time (n) is multiplied only by carrier freq; # the frequency deviation is added afterwards. signal_fm = math.cos(2 * math.pi * FM_CARRIER * (n / 44100.0) + 2 * math.pi * FM_BAND * integ_base / 44100.0) fm.writeframes(struct.pack('h', signal_fm * 32767))

Since the input signal influences the frequency directly, it goes "inside" the cosine operation. The major trick is: time multiplies the base carrier part **only.**

I had a hard time making the modulator to work, because I was multiplying everything by "n" and the output was white noise. In the end, Wikipedia saved me :) FM modulation looks simpler than AM (less lines of code, at least); and it is! A FM transmitter can be built with a few cheap components. Of course the high frequency of commercial FM helps, too: no big coils, smaller antennas, etc.

On the other hand, FM demodulation is tricky. Even in DIY FM receivers, demodulation is carried out by a dedicated integrated circuit, like the TDA7000. There are several methods of FM demodulation. I have "invented" a new method for my Python FM demodulator. It is grossly suboptimal, but it works:

**1) pass FM signal through a ramp/bandpass filter,** from FM_CARRIER-FM_BAND to FM_CARRIER+FM_BAND. Signals near -FM_BAND are made weaker and signals near +FM_BAND are made stronger, linearly.

This converts the FM signal into a form of AM modulation, because the processed signal strength shows relationship with original signal's strength.

**2) detector and low-pass filter,** exactly as in an AM receiver.

Here is the code. It is quite long because I employed two FFT filters, one at FM-AM conversion, and another as a low-pass filter. The filters' "technology" came directly from the post about FFT filters.

#!/usr/bin/env python SAMPLE_RATE = 44100 # Hz FM_BAND = 1000.0 FM_CARRIER = 44100 / 8.0 LOWPASS = FM_CARRIER - FM_BAND HIGHPASS = FM_CARRIER + FM_BAND HIGHPASS2 = FM_BAND # Hz import wave, struct, math from numpy import fft FFT_LENGTH = 2048 OVERLAP = 512 FFT_SAMPLE = FFT_LENGTH - OVERLAP NYQUIST_RATE = SAMPLE_RATE / 2.0 LOWPASS /= (NYQUIST_RATE / (FFT_LENGTH / 2.0)) HIGHPASS /= (NYQUIST_RATE / (FFT_LENGTH / 2.0)) HIGHPASS2 /= (NYQUIST_RATE / (FFT_LENGTH / 2.0)) zeros = [ 0 for x in range(0, OVERLAP) ] # Make ramp filter for FM-AM conversion mask = [] for f in range(0, FFT_LENGTH / 2 + 1): if f < LOWPASS or f > HIGHPASS: ramp = 0.0 else: ramp = (f - LOWPASS) / (HIGHPASS - LOWPASS) mask.append(ramp) # make lowpass filter mask2 = [] for f in range(0, FFT_LENGTH / 2 + 1): if f > HIGHPASS2 or f == 0: ramp = 0.0 else: ramp = 1.0 mask2.append(ramp) fm = wave.open("fm.wav", "r") demodulated = wave.open("demod_fm.wav", "w") demodulated.setnchannels(1) demodulated.setsampwidth(2) demodulated.setframerate(SAMPLE_RATE) n = fm.getnframes() fm = struct.unpack('%dh' % n, fm.readframes(n)) # scale from 16-bit signed WAV to float fm = [s / 32768.0 for s in fm] saved_td = zeros intermediate = [] # Convert FM to AM for pos in range(0, len(fm), FFT_SAMPLE): time_sample = fm[pos : pos + FFT_LENGTH] frequency_domain = fft.fft(time_sample, FFT_LENGTH) l = len(frequency_domain) for f in range(0, l/2+1): frequency_domain[f] *= mask[f] for f in range(l-1, l/2, -1): cf = l - f frequency_domain[f] *= mask[cf] time_domain = fft.ifft(frequency_domain) for i in range(0, OVERLAP): time_domain[i] *= (i + 0.0) / OVERLAP time_domain[i] += saved_td[i] * (1.0 - (i + 0.00) / OVERLAP) saved_td = time_domain[FFT_SAMPLE:] time_domain = time_domain[:FFT_SAMPLE] intermediate += time_domain.real.tolist() # Detector "diode" intermediate = [ abs(sample) for sample in intermediate ] saved_td = zeros output = [] del fm # Filter the result, removing high frequencies for pos in range(0, len(intermediate), FFT_SAMPLE): time_sample = intermediate[pos : pos + FFT_LENGTH] frequency_domain = fft.fft(time_sample, FFT_LENGTH) l = len(frequency_domain) for f in range(0, l/2+1): frequency_domain[f] *= mask2[f] for f in range(l-1, l/2, -1): cf = l - f frequency_domain[f] *= mask2[cf] time_domain = fft.ifft(frequency_domain) for i in range(0, OVERLAP): time_domain[i] *= (i + 0.0) / OVERLAP time_domain[i] += saved_td[i] * (1.0 - (i + 0.00) / OVERLAP) saved_td = time_domain[FFT_SAMPLE:] time_domain = time_domain[:FFT_SAMPLE] output += time_domain.real.tolist() # Scale signal and write to WAV file output = [ int(sample * 32767) for sample in output ] demodulated.writeframes(struct.pack('%dh' % len(output), *output))