Qu

Modulation and Demodulation

The idea

A signal rarely travels at the frequency it was born at. A voice channel lives under 4 kHz; a Wi-Fi link, a cellular carrier, and a GPS timing signal all share the same air without colliding, because each was moved into its own slice of spectrum before it was radiated. Modulation is the operation that does the moving: it takes a baseband message and rides it on a much higher-frequency carrier, for two reasons that have nothing to do with the information itself.

The first is physics. An efficient antenna needs to be a sizeable fraction of the wavelength it radiates, conventionally a quarter-wave, \(L = \frac{1}{4}\cdot\frac{c}{f}\). A 3 kHz tone has a 25 km quarter-wave length; a 900 MHz carrier has an 8 cm one. The second is coexistence: give every transmitter its own frequency slice and a receiver tuned to that slice hears only what was meant for it.

Analog case — amplitude modulation. Let \(m(t)\) be the message, scaled to \([-1, 1]\), and \(\cos(2\pi f_c t)\) the carrier at the assigned frequency \(f_c\). AM multiplies the carrier's amplitude by the message:

\[ y(t) = \big(1 + m(t)\big)\cos(2\pi f_c t) \]

The carrier oscillates at \(f_c\), far too fast to radiate a baseband signal directly, but its peak amplitude traces \(m(t)\) exactly — the message survives, relocated into an envelope wrapped around a wave short enough to build an antenna for.

seed(1)
fs = 4000
t = (0 to 799) / fs
m = sin(2*pi*20*t)
fc = 200
y = (1 + 0.6*m) * cos(2*pi*fc*t)
envelope = 1 + 0.6*m

figure()
plot(t, y)
plot(t, envelope)
plot(t, -envelope)
xlabel("time (s)")
ylabel("amplitude")
title("Amplitude modulation: carrier and envelope")
legend("carrier y(t)", "envelope", "")
output
· plot: figure 1 (800 points)
· plot: figure 2 (800 points)
· plot: figure 3 (800 points)
-1.5 -1.0 -0.5 0 0.5 1.0 1.5 2.0 2.5 0 0.02 0.04 0.06 0.08 0.10 0.12 0.14 0.16 0.18 0.20 Amplitude modulation: carrier and envelope time (s) amplitude carrier y(t) envelope

Digital case — the constellation. Digital modulation places each symbol at a point in the complex plane rather than tracing a continuous envelope. Binary Phase-Shift Keying (BPSK) sends one bit as \(s \in \{-1, +1\}\) on the real axis. Quadrature schemes use both axes at once: Quadrature Phase-Shift Keying (QPSK) — a 4-point special case of Quadrature Amplitude Modulation (QAM) — packs one bit onto each axis,

\[ s = \frac{1}{\sqrt{2}}\big(b_I + j\,b_Q\big), \qquad b_I, b_Q \in \{-1, +1\}, \]

doubling the data carried per transmitted symbol for the same symbol rate. The receiver's whole job is to decide which constellation point was probably sent, given a noisy version of \(s\).

bpsk = [-1, 1]
bpsk_y = [0, 0]
qpsk_i = [1, -1, -1, 1] / sqrt(2)
qpsk_q = [1, 1, -1, -1] / sqrt(2)

figure()
panel(1, 2, 1)
scatter(bpsk, bpsk_y)
xlabel("in-phase")
ylabel("quadrature")
title("BPSK constellation")

panel(1, 2, 2)
scatter(qpsk_i, qpsk_q)
xlabel("in-phase")
ylabel("quadrature")
title("QPSK constellation")
output
· scatter: figure 4 (2 points)
· scatter: figure 5 (4 points)
-1.0 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1.0 -0.6 0 0.6 x=-1, y=0 x=1, y=0 BPSK constellation in-phase quadrature -0.6 -0.4 -0.2 0 0.2 0.4 0.6 -0.4 0 0.4 x=0.707, y=0.707 x=-0.707, y=0.707 x=-0.707, y=-0.707 x=0.707, y=-0.707 QPSK constellation in-phase quadrature

In Qu

Recovering an AM message. The receiver must pull \(m(t)\) back out of \(y(t)\) without already knowing it. Qu's hilbert builds the *analytic signal* \(a(t) = x(t) + j\,\mathcal{H}\{x(t)\}\), whose magnitude at every instant is exactly the input's envelope, whatever the carrier phase is doing underneath. For the AM signal above, that envelope is \(1 + 0.5\,m(t)\), so undoing the known offset and scale hands back the message:

seed(1)
fs = 48000
t = (0 to 4799) / fs
fc = 4000
m = sin(2*pi*200*t)
y = (1 + 0.5*m) * cos(2*pi*fc*t)
print("y[0:5] = {y[0:5]}")

envelope = abs(hilbert(y))
recovered = (envelope - mean(envelope)) / 0.5
err = rms(recovered - m)
print("recovered[0:5] = {recovered[0:5]}")
print("m[0:5]         = {m[0:5]}")
print("rms recovery error = {err:.2e}")

figure()
plot(t[0:300], m[0:300])
plot(t[0:300], recovered[0:300])
xlabel("time (s)")
ylabel("amplitude")
title("AM message vs. Hilbert-envelope recovery")
legend("original m(t)", "recovered")
output
y[0:5] = [1, 0.87736, 0.513084, 6.363446e-17, -0.526132, -0.922545]
recovered[0:5] = [-7.105427e-15, 0.026177, 0.052336, 0.078459, 0.104528, 0.130526]
m[0:5]         = [0, 0.026177, 0.052336, 0.078459, 0.104528, 0.130526]
rms recovery error = 1.32e-13
· plot: figure 6 (301 points)
· plot: figure 7 (301 points)
-1.0 -0.8 -0.6 -0.4 -0.2 0 0.2 0.4 0.6 0.8 1.0 0 1 2 3 4 5 6 ×10−3 AM message vs. Hilbert-envelope recovery time (s) amplitude original m(t) recovered
y[0:5] = [1, 0.87736, 0.513084, 6.363446e-17, -0.526132, -0.922545]
recovered[0:5] = [-7.105427e-15, 0.026177, 0.052336, 0.078459, 0.104528, 0.130526]
m[0:5]         = [0, 0.026177, 0.052336, 0.078459, 0.104528, 0.130526]
rms recovery error = 1.32e-13

An idealized, noiseless channel gives the message back to fourteen decimal places. This is *envelope detection*, historically done with a diode and a capacitor rather than a Hilbert transform — the analytic signal is the same idea done exactly instead of approximately.

A noisy QPSK link. Add channel noise directly to complex symbols and watch the constellation blur:

seed(3)
bits_i = randi(0, 1, 200)
bits_q = randi(0, 1, 200)
si = 2*bits_i - 1
sq = 2*bits_q - 1
symbols_c = (si + sq*1j) / sqrt(2)
noise_c = 0.35*(randn(200) + 1j*randn(200))
rx = symbols_c + noise_c
dec_i = (sign(real(rx)) + 1) / 2
dec_q = (sign(imag(rx)) + 1) / 2
errs = sum(abs(dec_i-bits_i)) + sum(abs(dec_q-bits_q))
print("QPSK bit errors out of {2*200} bits = {errs}")

figure()
scatter(real(rx), imag(rx))
xlabel("in-phase")
ylabel("quadrature")
title("Received QPSK constellation, SNR-limited")
output
QPSK bit errors out of 400 bits = 11
· scatter: figure 8 (200 points)
-1.5 -1.0 -0.5 0 0.5 1.0 1.5 -2.0 -1.5 -1.0 -0.5 0 0.5 1.0 1.5 x=1.258, y=-0.719 x=0.593, y=-0.707 x=0.846, y=0.528 x=-0.48, y=0.532 x=0.869, y=0.504 x=-0.971, y=-0.699 x=-0.567, y=0.907 x=-0.401, y=0.729 x=-0.975, y=-0.673 x=-1.099, y=-0.561 x=0.235, y=0.987 x=-0.743, y=1.123 x=0.823, y=-0.632 x=-0.108, y=-0.582 x=-0.101, y=-0.638 x=-0.736, y=0.274 x=-0.04, y=-1.076 x=-0.649, y=0.314 x=0.803, y=-0.779 x=-0.151, y=-1.047 x=-0.414, y=0.733 x=-0.095, y=0.257 x=0.911, y=0.883 x=-0.537, y=-0.58 x=0.69, y=0.395 x=-0.471, y=0.993 x=0.75, y=0.363 x=0.929, y=0.29 x=-0.685, y=-0.608 x=0.452, y=0.484 x=-0.11, y=0.888 x=0.519, y=1.196 x=-0.609, y=-0.765 x=1.008, y=-1 x=-1.51, y=0.581 x=0.853, y=0.637 x=-0.436, y=-0.852 x=-1.418, y=-0.951 x=-0.314, y=-0.004 x=0.78, y=-0.749 x=-0.684, y=-1.261 x=0.667, y=0.531 x=-0.674, y=-0.938 x=-0.305, y=-0.708 x=0.707, y=-0.421 x=0.949, y=-1.246 x=0.163, y=0.567 x=-0.704, y=-1.268 x=-1.188, y=-0.443 x=-0.829, y=0.443 x=1.479, y=-0.795 x=1.124, y=1.15 x=0.469, y=1.264 x=-0.333, y=-0.48 x=0.492, y=-1.801 x=-0.843, y=-0.836 x=1.044, y=-0.533 x=-0.72, y=0.613 x=-0.529, y=-0.592 x=0.865, y=-0.862 x=-1.048, y=-0.911 x=-0.101, y=-0.869 x=0.389, y=0.487 x=0.448, y=-0.215 x=0.555, y=1.219 x=-0.735, y=-0.407 x=-0.747, y=-0.716 x=0.424, y=0.469 x=0.423, y=0.266 x=0.713, y=1.346 x=0.899, y=-0.678 x=1.654, y=-0.969 x=0.629, y=-0.835 x=0.7, y=-0.848 x=0.269, y=0.346 x=-1.064, y=1.203 x=0.605, y=-0.935 x=0.808, y=0.823 x=-0.121, y=0.694 x=-1.052, y=-1.21 x=0.803, y=0.586 x=-0.223, y=1.042 x=0.684, y=1.154 x=-0.718, y=-0.713 x=-1.249, y=-0.107 x=-0.396, y=-0.344 x=0.319, y=0.493 x=-0.754, y=-0.539 x=1.205, y=-0.2 x=0.407, y=-0.54 x=1.233, y=0.037 x=-0.669, y=1.011 x=-0.233, y=-0.829 x=-0.839, y=1.248 x=1.038, y=-0.111 x=-1.997, y=-0.837 x=1.138, y=0.705 x=-0.874, y=0.923 x=0.472, y=1.049 x=0.576, y=-0.703 x=-0.473, y=1.091 x=-0.903, y=0.992 x=-1.115, y=1.113 x=0.471, y=0.608 x=-0.983, y=1.014 x=-1.064, y=0.985 x=0.673, y=-0.757 x=-0.631, y=-0.169 x=-0.271, y=0.959 x=-0.874, y=0.706 x=-1.155, y=-0.644 x=0.042, y=-1.144 x=0.662, y=-0.544 x=0.318, y=0.488 x=0.501, y=-0.648 x=0.775, y=-0.552 x=-0.297, y=1.356 x=-0.717, y=0.68 x=-0.308, y=1.238 x=0.338, y=0.767 x=-0.979, y=1.262 x=-0.959, y=1.022 x=1.223, y=-0.304 x=1.039, y=-0.265 x=0.825, y=0.936 x=0.312, y=-0.284 x=-0.672, y=-0.98 x=-0.617, y=0.714 x=0.856, y=0.531 x=0.785, y=-0.478 x=-1.343, y=0.471 x=0.403, y=0.747 x=-0.197, y=0.641 x=-0.788, y=-1.667 x=1.414, y=0.148 x=0.853, y=0.585 x=-1.08, y=-0.57 x=-1.263, y=-0.444 x=1.335, y=1.126 x=-0.835, y=-0.259 x=-0.889, y=0.901 x=-0.09, y=-0.816 x=0.38, y=0.986 x=-0.401, y=-0.347 x=-0.536, y=-0.395 x=-1.147, y=-0.752 x=-0.929, y=0.108 x=-1.374, y=-0.313 x=-1.324, y=1.462 x=-0.295, y=-1.29 x=-0.667, y=-0.734 x=0.41, y=-0.658 x=-0.525, y=-1.034 x=-0.635, y=-0.785 x=0.4, y=-1.211 x=0.919, y=-0.979 x=1.046, y=-0.776 x=1.14, y=0.862 x=-0.315, y=-0.5 x=0.016, y=0.779 x=-0.899, y=-0.588 x=0.287, y=-0.5 x=0.458, y=-0.487 x=0.84, y=0.439 x=-1.217, y=0.558 x=1.129, y=1.365 x=-0.686, y=1.288 x=0.7, y=-0.525 x=1.051, y=-1.264 x=0.879, y=0.016 x=-1.043, y=0.008 x=0.305, y=1.448 x=-0.625, y=0.841 x=0.758, y=0.352 x=0.335, y=-1.105 x=-0.962, y=-0.398 x=-0.535, y=-0.948 x=-0.363, y=-1.038 x=0.691, y=-0.407 x=0.09, y=-1.014 x=0.589, y=1.462 x=0.686, y=-1.311 x=-0.858, y=1.109 x=0.45, y=-0.414 x=0.364, y=-0.953 x=-0.384, y=-0.825 x=0.524, y=0.237 x=0.057, y=0.564 x=-0.768, y=-0.465 x=0.874, y=-0.174 x=-0.745, y=-1.415 x=-0.939, y=-1 x=-0.527, y=-0.863 x=0.71, y=1.662 x=0.563, y=-0.2 x=1.074, y=-0.759 x=0.728, y=-0.497 x=-0.526, y=-0.937 x=-1.214, y=-0.927 x=-0.885, y=0.785 Received QPSK constellation, SNR-limited in-phase quadrature
QPSK bit errors out of 400 bits = 11

Eleven of 400 bits flip once the four clean corners smear into overlapping clouds — the receiver still decides correctly most of the time by asking each axis's sign, but the clouds now reach across the decision boundary at zero often enough to matter. Qu has no qam_modulate or bpsk_demod: there is no dedicated digital-modulation function in the standard library, only the general-purpose complex arithmetic, randn, and sign used above, plus pwm (natural-sampling pulse-width modulation) for the one specialized scheme it does ship. That is an honest description of where the library draws its line, not a gap in this lesson — every digital modulation scheme, at its core, is exactly the four lines above: map bits to a point in the plane, add the channel, decide which point was probably sent.

Both examples above skipped straight from "symbol sent" to "symbol plus noise received," as though the medium did nothing but add a little static. Real channels do far worse: a signal arrives by several paths at once, at different delays and strengths, and comes out reshaped before any noise is added. That reshaping is a convolution, and undoing it is next.

Next: Lesson 14 — Channels and equalization.