In this notebook, we will introduce the basic OFDM modulation and demodulation in complex baseband and describe the difficulties that arise in a real-world practical implementation.

For a more detailed explanation of the basic OFDM system have a look at the article at DSPIllustrations.com. Basically, one OFDM symbol of length $T$ consists of $K$ subcarriers, where each carrier conveys a single complex-valued QAM symbol. The subcarriers are $1/T$ apart to ensure orthogonality between the carriers. The modulation is usually performed using the inverse Fourier Transform, and at the receiver side an Fourier transform is applied to recover the data.

In addition, a cyclic prefix is added to each symbol in order to combat inter-symbol interference induced by a multipath channel and to transform the linear convolution with the channel's impulse response into a circular convolution.

Mathematically, the OFDM transmit signal is obtained as follows. We define the QAM symbols $X$ in the frequency domain. Then, the time-domain transmit signal $x[n]$ is obtained by

$$ x[n] = \sum_{k=0}^{K-1} X[k]\exp(j2\pi kn/K),\quad n=-N_{CP},\dots, K-1 $$By taking a closer look, we see that the summation is actually a discrete Fourier transform, and hence we can write

$$ x[n] = K\cdot \text{IDFT}_K\{X[k]\}[n], \quad n=0,\dots,K-1 $$Afterwards, a CP is added to the signal.

At the receiver side the inverse operation is performed to retrieve the transmitted symbols:

$$ \hat{X}[k] = \sum_{n=0}^{N-1}x[n]\exp(-j2\pi kn/K) $$Taking a closer look, above expression is equal to a Discrete Fourier Transform and hence

$$ \hat{X}[k] = \text{DFT}_K\{x[n]\}[k] $$If we now insert the expression for the transmitted signal we get

$$ \hat{X}[k] = \text{DFT}_K\{K\cdot\text{IDFT}_K\{X[k]\}[n]\}[k] =K X[k]$$which shows that the received data equals the transmitted data up to a scaling factor.

Consider now, that the transmitted signal is sent through a multipath channel with impulse response $h[n]$. At the receiver side we have

$$ \hat{X}[k] = K\cdot \text{DFT}_K\{h[n]*\text{IDFT}_K\{X[k]\}[n]\}[k] $$Now, using the convolution theorem (convolution in time is multiplication in frequency domain), this expression leads to

$$ \hat{X}[k] = K\cdot \text{DFT}_K \{\text{IDFT}_K\{H[k]X[k]\} \} = K H[k]X[k] $$where $H[k]$ is the DFT of the impulse response. *Note that above derivation only works for a sufficiently long CP that transform the linear convolution of the channel into a circular convolution.*

Hence, what we see is that the received symbols are equal to the transmitted symbols multiplied by the frequency response of the channel. There is no interference and we can use a simple one-tap equalizer.

Now that we have talked about the fundamentals, let's start implementing the modulation and demodulation:

In [4]:

```
from audioComms.components import Component, TX, Environment, Recorder
from audioComms.utils import StatefulFilter, get_filter
from audioComms.plotting import PlotWaveform, PlotSpectrum, PlotConstellation, PlotBlock
from audioComms.channels import AudioChannel, SimulatedChannel, IdentityChannelEffect, SimulatedChannelEffect, NoiseChannelEffect
```

In [5]:

```
# The OFDM parameter structure describing the OFDM symbol
class OFDM:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
ofdm = OFDM()
ofdm.K = 256 # Number of OFDM subcarriers
ofdm.Kon = 200 # Number of switched-on subcarriers
ofdm.CP = 64 # Number of samples in the CP
```

Here, you might wonder what the parameter Kon means. It's the number of subcarriers that actually carry data. In practice, guard bands are left at both sides of the OFDM spectrum, such that not the full spectrum is used for data transmission. The used carriers are located around the DC frequency, whereas the guards appear at the edge of the band. We will shortly, what this means.

First, let's focus on the modulation. To modulate some data, we need to have a random data generator:

In [6]:

```
def random_qam(ofdm):
qam = np.array([1+1j, 1-1j, -1+1j, -1-1j]) / np.sqrt(2)
return np.random.choice(qam, size=(ofdm.Kon), replace=True)
random_qam(OFDM(K=8, Kon=4))
```

Out[6]:

In [7]:

```
# Source code has been redacted in online version
# Omitting 10 lines of source code
```

Let's run this function with some example data and look at the spectrum of the output:

In [8]:

```
ofdm = OFDM(K=64, Kon=21, CP=16)
tx = ofdm_modulate(ofdm, random_qam(ofdm))
f = np.linspace(-ofdm.K/2, ofdm.K/2, len(tx)*8, endpoint=False)
plt.figure(figsize=(8,3))
plt.plot(f, abs(np.fft.fftshift(np.fft.fft(tx, 8*len(tx)))))
plt.grid(True); plt.xlabel('Subcarrier index'); plt.ylabel('|X(f)|')
plt.gca().add_patch(patches.Rectangle((-10,0), 20, 20, linewidth=1, fill=None, hatch='/'));
```

In [9]:

```
class OFDMTransmitter(TX):
def __init__(self, environment, ofdm):
super().__init__(environment)
self._ofdm = ofdm
def _generateSignal(self):
qam = random_qam(self._ofdm) # Create random data
sym = ofdm_modulate(self._ofdm, qam) # Perform OFDM modulation
return sym
```

In [10]:

```
def runTransmission():
env = Environment(samplerate=1000)
ofdm = OFDM(K=256, Kon=128, CP=64)
tx = OFDMTransmitter(env, ofdm)
channel = SimulatedChannel(env)
fig = env.figure(figsize=(10,3))
showWave = PlotWaveform(env, windowDuration=256+64, integerXaxis=True, isComplex=True, axes=fig.add_subplot(121), ylim=(-2,2))
showSpec = PlotSpectrum(env, windowDuration=0.5, axes=fig.add_subplot(122), logScale=True)
tx.transmitsTo(channel)
channel.transmitsTo(showWave)
channel.transmitsTo(showSpec)
env.run()
runTransmission()
```

In [11]:

```
# Source code has been redacted in online version
# Omitting 31 lines of source code
```

In [12]:

```
# Source code has been redacted in online version
# Omitting 15 lines of source code
```

As the OFDM signal is a complex-valued signal, we need to transform it into the passband to a certain carrier frequency such that we can transmit it over a real channel. As we have already prepared the up- and downconversion components it is simply a matter of plugging them together to reach our goal:

In [13]:

```
from audioComms.passband import InterpolationUpconversion, DownconversionDecimation
```

Let us just include the up- and downconversion and see how the system works:

In [14]:

```
def runTransmission(channelFunc):
env = Environment(samplerate=44100)
ofdm = OFDM(K=256, Kon=128, CP=64)
B = 5*441 # Baseband bandwidth in Hz
Fc = 10000 # carrier frequency in Hz
tx = OFDMTransmitter(env, ofdm)
upc = InterpolationUpconversion(env, B=B, Fc=Fc)
channel = channelFunc(env)
down = DownconversionDecimation(env, B=B, Fc=Fc)
rx = MostBasicOFDMReceiver(env, ofdm)
constellation = PlotConstellation(env)
tx.transmitsTo(upc)
upc.transmitsTo(channel)
channel.transmitsTo(down)
down.transmitsTo(rx)
rx.transmitsTo(constellation)
env.run()
runTransmission(SimulatedChannel)
```

In [15]:

```
from audioComms.channels import InitialLossChannelEffect
runTransmission(lambda env: SimulatedChannel(env, InitialLossChannelEffect(int((3+3)*44100/(5*441)))))
```

Up to now, we only used the deterministic simulated channel. Now, let's try with the system with using the audio signal:

In [16]:

```
runTransmission(AudioChannel)
```

In this notebook, we have developed a simple OFDM transmitter and most basic receiver. We have seen that random delay in the channel hinders the receiver to extract the correct symbols from the received signal and hence cannot obtain the transmitted constellation.

What we need is a method to automatically determine the starting point of the OFDM symbols. This, so-called **synchronization** will be the focus of the upcoming notebook.

DSPIllustrations.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com, amazon.de, amazon.co.uk, amazon.it.