FM modulation

FM (Frequency Modulation) is the most common audio analog modulation in use, either in commercial stations, ham radio and walkie-talkies. (A big exception is airband, which is AM).

Even the digital radio modes like DMR and D-Star are FM-based to some extent, which allows for cheaper circuits and pacific coexistence of analog and digital modes in the band.

In this article, I try to show how a baseband signal is transformed under FM modulation using a Python implementation.

The baseband is a filtered audio with my voice, already employed in the article about AM modulation:

Now, it is FM-modulated with a 10kHz carrier. The bandwidth of the baseband is limited to 1kHz, and the maximum frequency deviation was also limited to 1kHz. Please don't listen the result in high volume, it could make your head explode!

It seems impossible to recover voice from this mess, but indeed it can be recovered. There is some loss in quality due to the filters at the "receptor":

Now, let's see how this is carried out. First, a little bit of theory. FM modulation is based on tweaking the carrier frequency, based on input signal strength.

The modulated output has **constant power envelope**, even when the signal is
pure silence. This makes FM very noise-resistant. Is also allows for a simpler
and more efficient transmitter.

In AM, and even more in SSB, the output power has a linear relationship with the input. This lets the signal vulnerable to noise during low-power stints. The transmitter must be more complex and less efficient, because it must linearly modulate the output.

An analog FM transmitter may be incredibly simple. Using a varicap oscillator (varicap is a diode that works as a voltage-controlled variable capacitor), the input can directly control the varicap, and the oscillator frequency.

Digital realization is a little more tricky. One technique is to rush or drag the carrier's phase accordingly to the input. Continuously "rotating" the carrier's phase is the same as deviating the frequency.

The crucial step is to relate the input with phase deviation, so the output frequency does not go beyond the bandwidth (in the examples, it is 9kHz-11kHz). The input signal must be integrated, since the phase deviation is left as it is, rather than reset, when the input is silenced.

Instead of tweaking the carrier phase directly, we translate the phase to cartesian coordinates (I/Q) and then use a quadrature modulator. This is the bread-and-butter technique for software-defined radios.

#!/usr/bin/env python3 # FM modulator based on I/Q (quadrature) modulation import wave, struct, math input_src = wave.open("paralelepipedo_lopass.wav", "r") FM_CARRIER = 10000.0 MAX_DEVIATION = 1000.0 # Hz fm = wave.open("iq_fm.wav", "w") fm.setnchannels(1) fm.setsampwidth(2) fm.setframerate(44100) phase = 0 # in radians for n in range(0, input_src.getnframes()): # rush or drag phase accordingly to input signal # this is analog to integrating inputsgn = struct.unpack('h', input_src.readframes(1))[0] / 32768.0 # translate input into a phase change that changes frequency # up to MAX_DEVIATION Hz phase += inputsgn * math.pi * MAX_DEVIATION / 44100 phase %= 2 * math.pi # calculate quadrature I/Q i = math.cos(phase) q = math.sin(phase) carrier = 2 * math.pi * FM_CARRIER * (n / 44100.0) output = i * math.cos(carrier) - q * math.sin(carrier) fm.writeframes(struct.pack('h', int(output * 32767)))

At receiving side, FM is much more complicated than AM. There are no FM "crystal sets". Even the DIY kits employ some ASIC like TDA7000. (Commercial FM has the additional complication of stereo audio, conveniently sorted out by the TDA7000 as well.)

There are many analog and digital methods to demodulate FM. In our implementation, we use a bread-and-butter digital recipe. A quadrature demodulator extracts the I/Q values, which are converted to polar angle, and the phase rotation is monitored to extract the original audio.

An important aspect is the low-pass filtration of I/Q intermediate signal. As mentioned in AM modulation text, any demodulation creates high-frequency copies of the signal. This is just a nuisance in AM, but it would prevent signal recovery in FM, so we must filter.

#!/usr/bin/env python3 # FM demodulator based on I/Q (quadrature) import wave, struct, math, random, filters input_src = wave.open("iq_fm.wav", "r") FM_CARRIER = 10000.0 MAX_DEVIATION = 1000.0 # Hz demod = wave.open("iq_fm_demod.wav", "w") demod.setnchannels(1) demod.setsampwidth(2) demod.setframerate(44100) # Prove we don't need synchronized carrier oscilators initial_carrier_phase = random.random() * 2 * math.pi last_angle = 0.0 istream = [] qstream = [] for n in range(0, input_src.getnframes()): inputsgn = struct.unpack('h', input_src.readframes(1))[0] / 32768.0 # I/Q demodulation, not unlike QAM carrier = 2 * math.pi * FM_CARRIER * (n / 44100.0) + initial_carrier_phase istream.append(inputsgn * math.cos(carrier)) qstream.append(inputsgn * -math.sin(carrier)) istream = filters.lowpass(istream, 1500) qstream = filters.lowpass(qstream, 1500) last_output = 0 for n in range(0, len(istream)): i = istream[n] q = qstream[n] # Determine phase (angle) of I/Q pair angle = math.atan2(q, i) # Change of angle = baseband signal # Were you rushing or were you dragging?! angle_change = last_angle - angle # Just for completeness; big angle changes are not # really expected, this is FM, not QAM if angle_change > math.pi: angle_change -= 2 * math.pi elif angle_change < -math.pi: angle_change += 2 * math.pi last_angle = angle # Convert angle change to baseband signal strength output = angle_change / (math.pi * MAX_DEVIATION / 44100) if abs(output) >= 1: # some unexpectedly big angle change happened output = last_output last_output = output demod.writeframes(struct.pack('h', int(output * 32767)))

The filter is rather crude FIR filter and was borrowed from this article about FIR filters. It was good enough for the purposes of this article:

#!/usr/bin/env python import numpy, math from numpy import fft SAMPLE_RATE = 44100 # Hz NYQUIST_RATE = SAMPLE_RATE / 2.0 FFT_LENGTH = 512 def lowpass_coefs(cutoff): cutoff /= (NYQUIST_RATE / (FFT_LENGTH / 2.0)) # create FFT filter mask mask = [] negatives = [] l = FFT_LENGTH // 2 for f in range(0, l+1): rampdown = 1.0 if f > cutoff: rampdown = 0 mask.append(rampdown) if f > 0 and f < l: negatives.append(rampdown) negatives.reverse() mask = mask + negatives # Convert FFT filter mask to FIR coefficients impulse_response = fft.ifft(mask).real.tolist() # swap left and right sides left = impulse_response[:FFT_LENGTH // 2] right = impulse_response[FFT_LENGTH // 2:] impulse_response = right + left b = FFT_LENGTH // 2 # apply triangular window function for n in range(0, b): impulse_response[n] *= (n + 0.0) / b for n in range(b + 1, FFT_LENGTH): impulse_response[n] *= (FFT_LENGTH - n + 0.0) / b return impulse_response def lowpass(original, cutoff): coefs = lowpass_coefs(cutoff) return numpy.convolve(original, coefs) if __name__ == "__main__": import wave, struct original = wave.open("NubiaCantaDalva.wav", "r") filtered = wave.open("test_fir.wav", "w") filtered.setnchannels(1) filtered.setsampwidth(2) filtered.setframerate(SAMPLE_RATE) n = original.getnframes() original = struct.unpack('%dh' % n, original.readframes(n)) original = [s / 2.0**15 for s in original] result = lowpass(original, 1000) result = [ int(sample * 2.0**15) for sample in result ] filtered.writeframes(struct.pack('%dh' % len(result), *result))

Given that FM can be realized by rotating the phase of a carrier, we can understand FM as the analog version of phase modulation (PM), which is very common in the digital realm. Indeed, digital modes based on phase or frequency shift (PM, FSK, MSK, GMSK...) can all be realized through FM modulation/demodulation of a digital signal (square wave).

Naturally, real-world radios use better techniques, but using FM as front-end for digital receiving is good enough to listen DMR and D-Star signals easily and cheaply, using SDR and DSD.