Site menu FM modulation
e-mail icon
Site menu

FM modulation

e-mail icon

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:


(link to audio)

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!


(link to audio)

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


(link to audio)

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))
e-mail icon