EPx professional blog and repository for braindumps

2010/01/30

AGC control in Python modem, version 3 finished

This afternoon I put the last block into Python QAM "modem" version 3: Automatic Gain Control (AGC). The sources can be found at http://epx.com.br/wav, the m3*.py files, as well as gray.py as a dependency. The feature can be tested by e.g. changing volume of qam.wav file using Audacity.

AGC allows the modem to adapt to different power levels. Up to now, V3 modem was not adapting to the "volume" in WAV file; it assumed that TX always used 90% of dynamic range. V2 already had a crude amplitude measurer which only paid attention to the training header sequence. But a true AGC must adapt to changes mid-flight.

This is easier said than done. The main problem is that AGC must figure out the full dynamic range, that is, the maximum amplitude a symbol may have. But there is no guarantee that the "strongest" symbols will actually appear in QAM signal. And, if they appear, how does AGC "know" that they are the ones?

A crude solution would be to send the training sequence at maximum amplitude every few seconds, which is easily detectable as a no-data sound, but this wastes bandwidth and is not 1337 enough :)

The actual solution that real-world modems employ is to work with power level averages. It is trivial to calculate the average amplitude of our constellation. Then, we maintain a moving average of QAM signal reception, and compensate for the difference.

V3 uses a one-second moving average, which is short enough to profit from initial training sequence, and adapts fast enough while actual data is being received. TX was changed to send a training sequence at this average power level instead of the maximum possible power level, so AGC does not have to distinguish between training and data signal parts.

Then we have another problem: will actual signal power average match the theoretical constellation average? In theory, this will happen only if all symbols are equally probable. Real-world modems employ a randomizer on bit stream to ensure that.

I was optimistic that AGC would get a good average without a randomizer, but I was wrong. AGC will not work at all without a very random bit stream! It is incredible how ordinary data, like a text file, is so visibly non-random at such a low level.

So, out of necessity, I have implemented a very simple bit randomizer for V3, with a "4th order polynominal". This is a very pedantic way to say that randomizer takes into account the last 4 bits to figure out the next one, which is calculated with simple XOR operations. To be even more pedantic, I even employed a good programming practice this time: a self-test.


# Randomizer: 1 + x**-3 + x **-4

def randomize(b0):
global b1, b2, b3, b4
bs = b0 ^ (b3 ^ b4)
b1, b2, b3, b4 = bs, b1, b2, b3
return bs

def derandomize(b0):
global b1, b2, b3, b4
bt = b0 ^ (b3 ^ b4)
b1, b2, b3, b4 = b0, b1, b2, b3
return bt

rseq = [ int(random.random() * 2) for x in range(0, 25) ]
b1, b2, b3, b4 = (0, 0, 0, 0)
rseqS = [ randomize(b) for b in rseq ]
b1, b2, b3, b4 = (0, 0, 0, 0)
rseqT = [ derandomize(b) for b in rseqS ]
b1, b2, b3, b4 = (0, 0, 0, 0)

if rseqT != rseq:
print "Randomizer is broken"
print rseq
print rseqS
print rseqT
sys.exit(1)


This randomization scheme was copied directly from Fabio Montoro's "Modem e transmissão de dados" book.

It was funny to realize in practice how important the randomizer is, when you simply don't know the power level at RX side and must figure it out based on data stream itself.

Determining power level using averages, even with the help of randomizer, does not have the same precision as knowing it beforehand. This reduces the number of bits per symbol that modem can handle. V3 had reached 16 bits/symbol, now the highest reliable level is 12. (Turning off AGC in RX side makes it go back to 16.)

AGC was the last thing to implement in V3. The next major milestone, V4, will add some form of convolutional coding to increase noise resistance.

2010/01/29

Reaching 48000 bps



The graph above is the waveform for the 38400 bps QAM signal. While I believe in math, my common sense refuses to believe that any information can be recovered from such a chaotic wave.

The "version 3" QAM modem written in Python is a real winner. Today, some of the FIXMEs in code were implemented, and the thing reached 38400 bps at 2400 baud, and 48000 bps at 3000 baud -- both using 16 bits per symbol. Note that 3000 baud is almost twice the rate of 1800Hz carrier!

It "wanted" to work at 18 bits/symbol, but showed too many errors. I am not sure whether 16-bit WAV quantization is the hard limit here, but it seems to be the case. I was quite surprised to see that transmitted message was still recognizable, albeit corrupted.

The changes in v3 which allowed this performance jump are:

1) added code to detect adjacent symbols which are equal in amplitude and phase. While all V3 constellation symbols cause a phase change, sometimes it is very small, especially if we are sending many bits per symbol.

The algorithm is simple: if a symbol change is not detected within the expected interval, the complex signal is sampled anyway. Idiot as it seems, it allows RX to handle up to 16 bits per symbol, while it handled only 7 before.

2) Added a carrier Hz compensation, PLL-like. Any difference between TX and RX carriers cause a "phase drift" in decoded QAM signal -- that is, phase shows a small but continuous rotation in absolutely every sample. This can be distinguished from sudden, time-limited symbol phase rotations, and compensated for.

I discovered that phase drift is more stable (and hence easier to handle) if RX carrier frequency is smaller than TX. If TX carrier is above TX, phase drift oscilates in a difficult way. So, now we make sure that RX carrier is always "low". For symbols with 16 bits each, RX can tolerate up to 15 Hz carrier deviation, which seems very good to me.

3) Constellation uses Gray code now, so adjacent points are guaranteed to have only one different bit. This "softens" errors (even if RX resolves to the wrong symbol, chances are just one bit will be corrupted), and helps whatever error-correction algorithms in use (currently we use none, but I plan to implement convolutional codes in V4).

Towards v4

Actually, the v3 performance numbers are getting meaningless; of course they are possible just because the medium is a noiseless 16-bit WAV file, whose Shannon capacity is almost 700 kbps. (The Shannon limit of a phone line is around 24kbps.)

The next challenge is to make RX work well on noisy and band-limited signals. Rates like 14400bps will be possible only by implementation of convolutional code, I guess.

2010/01/26

QAM modem: 16800 bps!

As I said in the last post, I was thinking about direct (time-domain) analysis of QAM signal at RX part, in order to allow baud rates comparable to carrier frequency. I did not plan to put that into use... but I could not resist doing that.

The thing was working sooner than expected, and performance went beyond my expectations. It can handle 2400 baud with a 1800Hz carrier, like faster modems used to do. It can currently handle 7 bits per symbol, which translates to 16800bps. Going beyond this will demand improvements in symbol detection, which is currently the same as V2 (which is suboptimal).

V3 modem is basically the same as V2, except by decodification of QAM into complex samples, which V2 did in a "classical" fashion (demodulation + FIR filtering), and V3 does by direct analysis of QAM signal. So, I will list only the relevant V3 RX part:


# Proof that we don't need exact carrier frequency
CARRIER += random.random() * 20 - 10
# Proof that we don't need exact carrier phase
ra = random.random() * 2 * math.pi

symbols = []
f = CARRIER * 2 * math.pi / SAMPLE_RATE
carrier_90 = SAMPLE_RATE / CARRIER / 4.0
carrier_90 = int(carrier_90)
phase_error = 2 * math.pi * CARRIER / SAMPLE_RATE / 2.0
recovered = []

for t in range(0, len(qam) - carrier_90 - 2):
# Take derivatives of now and 90 degrees in future
d = (qam[t+1] - qam[t]) / f
d90 = (qam[t + carrier_90 + 1] - qam[t + carrier_90]) / f

st = math.sin(f * t + ra)
ct = math.cos(f * t + ra)

a = d90 * ct + d * st
b = d90 * st - d * ct

try:
tphase = -b / a
phase = math.atan(tphase)
except ZeroDivisionError:
phase = math.pi / 2
if (-b * a) < 0:
phase = -phase

if (abs(d) > abs(d90)):
amplitude = -d / (st * math.cos(phase) + ct * math.sin(phase))
else:
amplitude = -d90 / (-st * math.sin(phase) + ct * math.cos(phase))

if amplitude < 0:
amplitude = -amplitude
phase += math.pi

phase += math.pi * 2
phase %= math.pi * 2

cpx = crect(amplitude, phase)
# print t, int(phase * 180 / math.pi) % 360, cpx

recovered.append(cpx)


This technique is based on the fact that QAM signal is generated by a very simple formula:

s(t) = m . cos(2πft + p)

where "m" and "p" are the polar coordinates of constellation symbol being currently transmitted. The derivative of that is also a simple formula:

s'(t) = 2πf.m.sin(2πft + p)

The QAM decoding problem is: given s(t), find the unknown variables "m" and "p". We need two samples of s'(t) which are near enough to belong to the same symbol, to make an equation system and solve for "m" and "p".

That's exactly what the piece of code listed above does. The samples are taken 90 degrees apart because the equations would simplify very nicely.

I chose the s'(t) equation instead of s(t) because the QAM signal may not be centered at zero; that is, it may have some offset, or DC component. But the difference between each sample and the next is unaffected by the offset. "Difference" in a discrete signal is equivalent to "derivative" in a continous signal.

It is important to measure samples EXACTLY 90 degrees apart, which means that the "distance" between samples must be an integer number. This implies that WAV must be a multiple of 1800 / 4 = 450 Hz. Using a WAV of 43200 Hz works beautifully, while a WAV of 45000 Hz, which is better in theory and matches carrier, will not bear more than 4 bits/symbol. 44100 Hz won't work perfectly even for 3 bits/symbol.

Maybe I have "cheated" by choosing a "perfect" WAV sampling rate, but I suspect that a real-world modem does just that. Anyway, this technique of mine is not intended to be the best in town, it is just an exercise to try to go beyond "classical" QAM. Certainly there are ways to compensate for non-perfect alignment of carrier and sampling rate,

2010/01/25

New version of QAM modem, and ideas for the future

As I promised, I rewrote parts of Python QAM modem to use complex numbers, which leaves code much shorter and clearer (provided that you understand complex numbers). Also, I put the common code in a separate file, to avoid redundancy in TX and RX parts. The FIR lowpass filter was improved too.

The files are m2co.py, m2tx.py and m2rx.py. All of them can be found at ttp://epx.com.br/wav folder.

Other ideas

I think I went far enough with this. I was planning to write a "version 3", but I am not sure I will. Versions 1 and 2 already make good classroom examples. FWIW I will tell what's in my mind for v3, maybe someone else picks it up.

First, the "classical" demodulation approach showed its limits in version 2; baud rate can not go above 1000 or 1100 Hz for a carrier of 1800 Hz. The FIR filter has a trade-off between frequency precision and time-domain precision. Increasing baud rate demands increasing frequency precision, but then symbols begin to smear together after filtering. In order to reach V.29 or V.32 baud rate (which is almost the same as carrier frequency), some other detection approach must be employed.

I played with some formulas, remembered some trigonometry identities at Wikipedia, and came up with one idea. Since the derivative of QAM signal is well-defined by a simple formula, and there are two unknown variables that shape the QAM signal -- amplitude and phase -- it is possible to "invert" the formula and find amplitude and phase given two derivatives taken from time-domain signal.

The distance between measured derivatives could be e.g. 90 degrees to make formula "inversion" even easier. BTW derivative of time-domain QAM signal is just the difference between two consecutive samples. If one wants to make it prettier, could add a moving average to this direct measure.

This approach worked on spreadsheet; it would probably work in a Python implementation. It seems possible to determine symbol in half carrier wavelength at the most, normally a quarter wavelength is enough, which means that decoding is possible when baud rate >= carrier.

Another advantage of this technique, or any other that analyzes the QAM signal directly, is more resilience to small carrier deviations. They would appear just as a very small but persistent phase delay in every sample, which can be ignored or compensated for.

I have no idea if this is the technique that advanced modems actually use.

2010/01/22

QAM modem - RX part

Following the discussion of a "QAM modem" written in Python, let's take a look into the reception (RX) code.

Decoding the QAM signal (reliably, at least) is much more complicated than generating it, and RX has much more code. Because of that, I will explain each section separately.

First, some boilerplate code:


#!/usr/bin/env python

import wave, struct, math, numpy, random

SAMPLE_RATE = 44100.0 # Hz
CARRIER = 1800.0 # Hz
BAUD_RATE = 360.0 # Hz
PHASES = 8
AMPLITUDES = 4
BITS_PER_SYMBOL = 5

# FIR lowpass filter. This time we calculated the coefficients
# directly, without resorting to FFT.
lowpass = []
CUTOFF = BAUD_RATE * 4
w = int(SAMPLE_RATE / CUTOFF)
R = (SAMPLE_RATE / 2.0 / CUTOFF)
for x in range(-w, w):
if x == 0:
y = 1
else:
y = math.sin(x * math.pi / R) / (x * math.pi / R)
lowpass.append(y / R)

# Modem constellation (the same as TX side)
constellation = {}
invconstellation = {}
for p in range(0, PHASES):
invconstellation[PHASES - p - 1] = {}
for a in range(0, AMPLITUDES):
s = p * AMPLITUDES + a
constellation[s] = (PHASES - p - 1, a + 1)
invconstellation[PHASES - p - 1][a + 1] = s

qam = wave.open("qam.wav", "r")
n = qam.getnframes()
qam = struct.unpack('%dh' % n, qam.readframes(n))
qam = [ sample / 32768.0 for sample in qam ]


Part of the code is borrowed from TX: modulation parameters, constellation generator etc. Those things would fit better in a separate module, but I was lazy to do it :) Constellation loop generates a reverse-map dictionary, since we are doing the inverse of TX (given amplitude and phase, gimme the bits).

A new thing is the FIR filter generator. This time I did not use FFT to generate the coefficients; a closed-form expression was used instead. I am aware that this FIR filter is not the best possible (it ends abruptly at both sides), but served well enough for this toy.


# Demodulate in "real" and "imaginary" parts. The "real" part
# is the correlation with carrier. The "imaginary" part is the
# correlation with carrier offset 90 degrees. Having both values
# allows us to find QAM signal's phase.

real = []
imag = []
amplitude = []

# This proofs that we don't need to replicate the original carrier
# (at least, not in the same phase). Sometimes this random carrier
# phase makes the receiver to interpret training sequence as a
# character, but nothing serious.

t = int(random.random() * CARRIER)
# t = -1

# In the other hand, frequency must match carrier exactly; if we
# need to allow for frequency deviations, we must build a digital
# equivalent to PLL / VCO oscilator.

for sample in qam:
t += 1
angle = CARRIER * t * 2 * math.pi / SAMPLE_RATE
real.append(sample * math.cos(angle))
imag.append(sample * -math.sin(angle))
amplitude.append(abs(sample))

del qam


This was the "analog" part of the decoder. It is the most important, and the easiest to do, because trigonometry does so much for us.

Since we did a kind of AM modulation at TX, multiplying a digital signal by a sinusoid, we do the same at RX: multiply again by the sinusoid to get the original data. We don't know exactly the phase of QAM signal, and we don't need to. We just multiply by two local carriers, which are identical except by being 90 degress offset -- math.cos() and math.sin().

Note that I set "t" with a random initial number. This is a way to guarantee that local carrier will NOT be in phase with TX, as it happens in real world. The rest of RX will have to work harder because of this, naturally.

Incredible as it may seem, output of this demodulation is the X,Y position of symbol in modem constellation. The computed positions will be rotated, because local carrier is not in phase with original TX carrier, but since we have integrated the phase changes at TX, we just need to look for phase DIFFERENCES.

Finally, we "demodulated" amplitude in a separate channel, using abs(), just out of laziness, since we could obtain amplitude from real and imaginary parts.

It looks easy because we don't need to cope with frequency distortions. In practice, the local carrier would have to "tune" to the input signal, because AM demodulation depends on local carrier being EXACTLY the same frequency as modulated signal. And all transmission media (except our WAV file, of course) distort frequency in some degree.


# Pass all signals through lowpass filter
real = numpy.convolve(real, lowpass)
imag = numpy.convolve(imag, lowpass)
amplitude = numpy.convolve(amplitude, lowpass)

# After lowpass filter, all three lists show something like
# a square wave, pretty much like the original bitstream.
# If you want to see the result of early detection phase,
# uncomment this:

# f = wave.open("teste.wav", "w")
# f.setnchannels(1)
# f.setsampwidth(2)
# f.setframerate(44100)
# bla = [ int(sample * 32767) for sample in imag ]
# f.writeframes(struct.pack('%dh' % len(bla), *bla))


Actually, AM demodulation leaves a high-frequency residue in original data, so we pass our FIR filter in all signals (real, imaginary and amplitude). After this filtration, the signals will show themselves as quasi-square waves, like this:



We have three square waves like this: real, imag, and amplitude. Now we have the mission of extracting the symbols out of them. First, we need to detect phase of symbols:


# Detect phase based on real and imaginary values

phase = []
wrap = 2.0 * math.pi * (PHASES - 0.5) / PHASES
for t in range(0, len(real)):
# This converts (real, imag) to an angle
angle = math.atan2(imag[t], real[t])
if angle < 0:
angle += 2 * math.pi
# Move angle near to 2 pi to 0
if angle > wrap:
angle = 0.0
phase.append(angle)


Since our constellation does not use complex numbers, the X,Y coordinates found by demodulator are not directly useful to search for the symbol, so we need to compute actual angles, which we do using atan2().


# Find maximum amplitude based on training whistle (1s), and
# measure how much our local carrier is out-of-phase in
# relationship to QAM signal

max_amplitude = 0.0
carrier_real = 0.0
carrier_imag = 0.0
for t in range(int(0.1 * SAMPLE_RATE), int(0.9 * SAMPLE_RATE)):
max_amplitude += amplitude[t]
carrier_real += real[t]
carrier_imag += imag[t]

max_amplitude /= int(0.8 * SAMPLE_RATE)
carrier_real /= int(0.8 * SAMPLE_RATE)
carrier_imag /= int(0.8 * SAMPLE_RATE)

skew = math.atan2(carrier_imag, carrier_real)

print "Carrier phase difference: %d degrees" % (skew * 180 / math.pi)

del imag
del real


Here we analyze the "training" whistle sent by TX, in order to determine the amplitude range, as well as the phase difference between our carrier and TX carrier. We do this in a very lazy way here, and it would not be ok for a real-world modem.

Any real-world medium, be it wireless or wire or optics, does distort amplitude, phase and frequency, so it wouldn't work to estimate both at the beginning of session and leave it that way. Real-world modems use a PLL to generate local carrier, which adjusts itself all the time accordingly to input conditions.

Too good our WAV file is a perfect medium :)


# Normalize/quantify amplitude and phase to constellation steps
qsymbols = []
for t in range(0, len(phase)):
p = phase[t]
a = amplitude[t]
a /= max_amplitude
a = int(a * AMPLITUDES + 0.49)

# Compensate for local carrier phase difference
# This ensures the best phase quantization (avoiding that
# quantization edge is too near a constellation point)

p += 2 * math.pi - skew
p /= 2 * math.pi
p = int(p * PHASES + 0.49) % PHASES

qsymbols.append((p, a))

del phase
del amplitude


Here we take the "analog" phases and amplitudes, and quantize them into constellation. At this point we "unskew" the angles, so the quantified values map directly to our constellation.

The problem now is: we have thousands of samples which contain just a handful of symbols. We need to detect how many symbols are there -- and where.


# Try to detect edges between symbols

edge = []
settle_time = int(SAMPLE_RATE / BAUD_RATE / 8)
settle_timer = -1
in_symbol = 0
first_symbol = 0
last_a = last_p = 0
for t in range(0, len(qsymbols)):
s = 0
p, a = qsymbols[t]
if in_symbol > 0:
# we presume we are flying over a valid symbol
in_symbol -= 1
elif a != last_a or p != last_p:
# change detected, look for stabilization in future
settle_timer = settle_time
elif settle_timer > 0:
# one sample closer a good symbol
settle_timer -= 1
elif settle_timer == 0:
# signal settled for (settle_time) samples;
# take the current signal as a valid symbol
settle_timer = -1
in_symbol = int(0.85 * SAMPLE_RATE / BAUD_RATE)
if t > 0.5 * SAMPLE_RATE:
# make sure we are not at the beginning
s = 1
if not first_symbol:
first_symbol = t
edge.append(s)
last_p = p
last_a = a


At this section, we count on the fact that most symbols show a distinctive "edge" between them. We annotate the points where this edge is present, for further reference.

Note that this step did not find all symbols, because the message may have repeated symbols with phase 0, which don't s how any discontinuity. This is a direct result of poor constellation choice; it would have been better to avoid phase-0 points, so we would find all symbols by looking for edges.


# Find symbols based on edges and known baud rate

symbols = []
expected_next_symbol = SAMPLE_RATE / BAUD_RATE
lost = 0

for t in range(first_symbol - int(SAMPLE_RATE * 0.1), len(qsymbols)):
p, a = qsymbols[t]
s = edge[t]

if s:
# amplitude/phase edge, strong hint for symbol
lost = 0
symbols.append([p, a])
else:
lost += 1

if lost > expected_next_symbol * 1.5:
# no edge yet, take this as a non-transition signal
lost -= expected_next_symbol
symbols.append([p, a])

del qsymbols
del edge


Here we finally detect the handful of symbols we are interested in, and we can throw out the enormous time-domain lists once and for all.

The strategy of symbol detector is simple. Every detected edge is a new symbol for sure. But, if no edge was found after some time (baud rate + 50% tolerance), we blindly assume that we have a repeated symbol there.

Even if TX and RX baud rates were a bit different, this strategy would be robust, because most symbols do have an edge between them, so the algorithm will not have to work blind for more than 2 or 3 symbols at a time.


print "%d symbols found" % len(symbols)

# Differentiate phase (because we had integrated it at transmitter)

last_phase = 0
for t in range(0, len(symbols)):
p = symbols[t][0]
symbols[t][0] = (p - last_phase + PHASES) % PHASES
last_phase = p


In TX we had integrated (summed up) the phase offsets, so now we have to do the opposite operation.


# Translate constellation points into bit sequences

bitgroups = []
for t in range(0, len(symbols)):
p, a = symbols[t]
try:
bitgroups.append(invconstellation[p][a])
except KeyError:
print "Bad symbol:", p, a, ", ignored"

# Translate bit groups into individual bits

bits = []
for bitgroup in bitgroups:
for bit in range(0, BITS_PER_SYMBOL):
bits.append((bitgroup >> (BITS_PER_SYMBOL - bit - 1)) % 2)


Symbol characteristics are traded for bit sequences, and finally we can grab the actual bits we were looking for. The modem demodulation part is done, but now we have to recover bytes from bits, like a RS-232 interface would do.


# Find bytes based on start and stop bits. This will make you feel
# like a poor RS-232 port :)

t = 0
state = 0
byte = []
bytes = []
while t < len(bits):
bit = bits[t]
if state == 0:
# did not find start bit
if not bit:
# just found start bit
state = 1
elif state == 1:
# found start bit, accumulating a byte
if len(byte) < 8:
byte.append(bit)
else:
# byte complete, test stop bit
if bit:
# stop bit is there
bytes.append(byte)
else:
# oops, stop bit not there, we were cheated!
# backtrack to the point fake start bit was 'found'
t -= 8
byte = []
state = 0
t += 1

# Make a string from the bytes

msg = ""
print "Bytes:",
for byte in bytes:
value = 0
for bit in range(0, 8):
value += byte[bit] << (8 - bit - 1)
msg += chr(value)
print value,

print
print msg


The serial-to-bytes loop looks for start bits (zeros) and stop bits (ones). Once it finds a bit sequence that satisfies these conditions, it cuts a byte. If either the start or stop bit is missing, it keeps reading bits until it gets synchronized again.

Amazingly, this thing works. The improvements I can imagine for this Python modem are:

1) Take a closer look at the FIR filter, choosing the best cutoff frequency and avoid abrupt ends;
2) Choose a better constellation, without 0-phase points, and use complex numbers;
3) Generate RX local carrier using a PLL (phase-locked loop), so it can "follow" frequency and phase distortions.
4) Measure amplitude range continually using AGC (automatic gain control)
5) Add a bit randomizer

QAM modem - TX part

If you are an old-timer computer guy like me... you are probably nostalgic of this noise:

http://epx.com.br/wav/qam_sample.wav

After playing with analog modulations, this time I wanted to play with some digital modulation. I thought about PSK and FSK, but they are quite simple, and are just particular cases of QAM -Quadrature Amplitude Modulation. The audio abovementioned was generated by my "modem", written in Python.

In a nutshell, writing the transmission (TX) part of a modem is easy. The big challenge is to write the reception (RX) part. As you will see, the RX code is far longer and more complicated.

I did not try to write the perfect modem, so this thing will probably not work over a ham radio link or any other non-ideal transmission medium. I will point out the shortcomings that I know of, and there are for sure a whole bunch of other problems that I don't even know of. It's just to have fun, after all.

QAM modulation takes a carrier of fixed frequency and changes phase as well as amplitude, accordingly to the bits that are to be sent. The output wave is something like this:



Note that QAM, like AM, never changes carrier frequency. It would not be possible to change frequency along with phase and amplitude, because they would smear each other. (The digital cousin of FM is FSK).

Each change in QAM signal may carry several bits. For example, if we establish that we have 4 possible phases (0, 90, 180 and 270 degrees), and 2 possible amplitudes (50% and 100%), we have 8 possible combinations, which allow us to send 3 bits per sample (log2(8)).

So we have this concept of "baud rate", which is the number of symbol changes per second. The data bit rate is the baud rate multiplied by the number of bits that each symbol can carry.

Of course, we need to establish a relationship between phases, amplitudes and transmitted bits in an orderly manner. The nost common way to do it is using a "constellation" graph like this:



In the image above, we have 3 constellations. At left, a very simple one, with only four points. If you draw a line from origin (coordinates 0,0) to a given point, the line length is the signal amplitude, and line angle is the phase change. We just need to attribute a different bit pattern for each point.

Since all four points of the leftmost constellation are equidistant from origin, we can say that this constellation is not really QAM; it is PSK (phase shift keying) because it only uses phase changes to send information The other constellations are more cluttered, and have points of several amplitudes.

WIthout further delay, here is the code of modem TX part:


#!/usr/bin/env python

import wave, struct, math

SAMPLE_RATE = 44100.0 # Hz
CARRIER = 1800.0 # Hz
BAUD_RATE = 360.0 # Hz
PHASES = 8
AMPLITUDES = 4
BITS_PER_SYMBOL = 5

# generate constellation of phases and amplitudes
# for each possible symbol, and attribute it to a
# certain bit pattern. I avoided deliberately the
# complex numbers, even though they make things
# easier when dealing with constellations.
#
# Make sure that all ones = maximum aplitude and 0 phase
constellation = {}
for p in range(0, PHASES):
for a in range(0, AMPLITUDES):
s = p * AMPLITUDES + a
constellation[s] = (PHASES - p - 1, a + 1)

# This is the message we want to send
msg = "Abracadabra!"
# msg = open("modem_tx.py").read()

# Convert message to a list of bits, adding start/stop bits
msgbits = []
for byte in msg:
byte = ord(byte)
msgbits.append(0) # start bit
for bit in range(7, -1, -1): # MSB
msgbits.append((byte >> bit) % 2)
msgbits.append(1) # stop bit

# Round bit stream to BITS_PER_SYMBOL
while len(msgbits) % BITS_PER_SYMBOL:
msgbits.append(0)

# Group bit stream into symbols
msgsymbols = []
for n in range(0, len(msgbits), BITS_PER_SYMBOL):
symbol = 0
for bit in range(0, BITS_PER_SYMBOL):
symbol += msgbits[n + bit] << (BITS_PER_SYMBOL - bit - 1)
msgsymbols.append(symbol)

# Add 1 second worth of all-1 bits in header, plus a trailer.
# The 11111 bit pattern generates a pure carrier, that receiver
# can recognize and train.
allones = (1 << BITS_PER_SYMBOL) - 1
train_sequence = [ allones for x in range(0, int(BAUD_RATE)) ]
finish_sequence = [ allones, allones ]
msgsymbols = train_sequence + msgsymbols + finish_sequence

qam = wave.open("qam.wav", "w")
qam.setnchannels(1)
qam.setsampwidth(2)
qam.setframerate(44100)

t = -1
phase_int = 0
samples = []
for symbol in msgsymbols:
amplitude = 0.9 * constellation[symbol][1] / AMPLITUDES
# Phase change is integrated so we don't need absolute phase
# at receiver side.
phase_int += constellation[symbol][0]
phase_int %= PHASES
phase = phase_int / (0.0 + PHASES)

# It would be easier to manipulate phase using complex numbers,
# but I thought this way would be clearer.

# Play the carrier at chosen phase/amplitude for 1/BAUD_RATE seconds
for baud_length in range(0, int(SAMPLE_RATE / BAUD_RATE)):
t += 1
sample = math.cos(CARRIER * t * math.pi * 2 / SAMPLE_RATE \
+ 2 * math.pi * phase)
sample *= amplitude
samples.append(int(sample * 32767))

qam.writeframes(struct.pack('%dh' % len(samples), *samples))

print "%d symbols sent" % len(msgsymbols)


Actually, the most complicated part of TX is the bit stream expansion. The original message is supplied as a string of bytes, so we need to "explode" it to individual bits, and add start and stop bits. The output signal itself is easy to generate, courtesy of math.cos() function :)

Do you remember that modems used to "whistle" for a couple seconds before connection? It was necessary to do the same here, so the receiver can get "trained" to the signal. Despite the fact the thing reads data from a high-quality sound file, the training phase is still needed.

I employed a 1800 Hz carrier just out of nostalgy, because this resembles the pitch of once-ubiguitous telephone modems.
The other parameters were found empirically; they are just below the level in which RX code fails to recover the message.

The constellation I generate would plot like a bicycle wheel, with "spokes" coming from origin to the outer rim. This is NOT an optimal constellation at all, because it is more cluttered near the origin, which means that those low-amplitude symbols are more easily misinterpreted in presence of noise. The constellations shown in second picture have better scattering.

There is another reason why my constellation is sub-optimal. I opted not to use complex numbers, to keep the code more readable for non-mathematically-inclined programmers. Unfortunately, making an optimal constellation without complex numbers means unreadable code too, so I chose to keep it simple and sub-optimal. (I plan to write a version of this QAM code using complex numbers in a future post.)

Yet another potential problem of my constellation, is that there are 0-degree points, meaning a symbol that does not change phase. This is difficult on RX because if such a symbol is sent repeatedly, the QAM signal will not have any "bump" between symbols, and RX will have to guess the symbol size. Another thing to fix in a new modem version.

Talking about phase, the phase change is integrated at TX side. For example, if you have several 90-degree symbols in sequence, the angles are accumulated, so the output signal will have 90, 180, 270, 0, 90, 180... phased symbols. At RX side, the phase CHANGE is taken into account, so the symbols are correctly interpreted as 90, 90, 90, 90... If phase were not integrated at TX, the RX side would have to know the ABSOLUTE phase all the time, and would lose forever the sync as soon as the first noise struck the signal.

Note that amplitude, by its nature, does not need to be integrated to be correctly interpreted by RX. A simple moving average at RX side is enough to know the amplitude range.

The bit stream can also be conditioned by a "randomizer". It avoids long sequences of 0s and 1s, thus avoiding long sequences of a single symbol, and making RX life easier. I did not implement a randomizer in this version, but perhaps I'll do in the next one.

2010/01/21

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, and probably a lot of people can't stand hearing it once more :)

paralelepipedo_lopass.wav

Now I modulate 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!

fm.wav

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

demod_fm.wav

If your browser can't play audio files using embed tag, you can find the audios at http://epx.com.br/wav/.

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 carrier output is always at maximum power, while AM output may be overwhelmed by noise when it is in a "valley". 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 :) Perhaps it wouldn't make a difference if carrier frequency was well above the input signal, but in this example they are fairly close (1Khz to 5KHz).

FM modulation looks simpler than AM (less lines of code, at least); and it is! A FM transmitter can be build with a few cheap components. Of course the high frequency of commercial FM helps in this too: no big coils etc.

In the other hand, FM demodulation is tricky; even in DIY FM receiveres the demodulation part is carried out by a custom integrated circuit, some TDAxxx. 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, in a proportional way.

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 FFT filter post.


#!/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))

2010/01/20

[POBRE] Reconfigurando modem SpeedTouch

No Mercado Livre há um "mar" de modems ADSL Thomson SpeedTouch ST510V6 para vender, e sempre por um preço muito baixo. Comprei um esses dias por R$ 15.

Não sei exatamente qual a razão do baixo preço (seriam modems em comodato?), mas uma desvantagem óbvia deste modem para um leigo é que não se pode configurá-lo via Web. Ou ele está configurado para sua operadora, ou não está. Mesmo em modo bridge, é preciso pelo menos configurar os parâmetros VPI e VCI da camada ATM, que variam conforme a operadora. Assim, um modem SpeedTouch pré-configurado está, na prática, "bloqueado".

Ele até tem modo telnet, que em tese permite mexer nestas configurações, mas a interface CLI é horrível. Embora eu tenha conseguido configurá-lo para usar apenas ADSL2, não consegui alterar VPI e VCI por este caminho.

Mas como tudo tem jeito exceto a morte e os impostos, é possível reconfigurar o modem usando um software externo, denominado "STInstall". Encontrei o tal programa seguindo a pista do site http://blog.mhavila.com.br/2006/09/17/roteador-adsl-speedtouch-510v6/.



Não foi muito fácil achá-lo pois a maioria dos links que oferecem o tal aplicativo estão quebrados. O seguinte link parece estar funcionando enquanto escrevo isto: http://speedtouch.com.br/st510v6/SpeedTouch510v6.zip. Parece que o programa também está disponível via Torrent.

O site brasileiro http://www.speedtouch.com.br não fornece o STInstall, mas apenas o STUpgrade, que serve para atualizar firwmare (mantendo a configuração "bloqueada" original).

Aparentemente, o STInstall era oferecido por uma telecom belga (que preferiu dar a seus usuários a possibilidade de reconfigurá-lo). Não é a primeira vez que um programa de telecom estrangeira salva o nosso dia; o modem 3G ZTE MF622 da Claro funciona com o "discador" Mac OS X da australiana Telstra (a Claro fornece o "discador" em CD mas não o disponibiliza para download em lugar nenhum, o que complica a vida se você não está de posse do CD).

Importante!

O programa é para Windows, mas funcionou bem no VMWare, colocando a rede em 'bridge'. Não vou colocar um link para download a partir do meu site pois realmente não sei qual é a situação de licença deste programa.

Além disso, ele sempre pode estar contaminado por algum keylogger ou coisa do gênero. Como eu só uso o Windows em VMWare, e só para esse tipo de bobagem, aceitei correr um risco calculado para fazer meu modem funcionar.

Se você usa Windows como sistema operacional principal, talvez fosse bom criar uma VM ou usar um outro computador só para reconfigurar o modem.

E o modem presta?

Diz a lenda que é um dos poucos modems que funciona bem em modo router, sem travar etc. Como eu tenho roteador Linux, uso sempre em modo bridge. Nos poucos testes que fiz, o meu modem "caro", o SpeedStream, parece ter desempenhado melhor. Adquiri o SpeedTouch para ter um modem-reserva que fosse ADSL2.

Uma curiosidade: este modem usa uma fonte de 22V, com um plug ligeiramente diferente do normal. Se for comprar um no ML, certifique-se que vem com a fonte, pois certamente suas fontes sobressalentes são de 5V ou 12V, mas não de 22V.

2010/01/18

IIR filter, in Python (as usual)

And now, for completeness, let's have an IIR (infinite Impulse Response) filter. Those digital filters calculate the output signal based not only on past input signals, but also on past OUTPUT signals. In other words, there is a feedback component on IIR output. This filter may have output forever, even if the input signal is cut, hence the "infinite" quality.

An IIR filter can be expressed by a difference equation, which defines output in terms of previous outputs, as well as inputs. A very popular example of difference equation is the Fibonacci sequence:


Fib(n) = Fib(n-1) + Fib(n-2)


In digital filtering, we are more interested in difference equations that are stable, that is, tend to zero as the input fades away. Since a functional IIR filter can be defined by a difference equation of just two terms, like the Fibonacci series above, it can do much more using much less math. In this sense, IIR is much more powerful than FIR.

The following example is a kind of resonator that enhances frequency response around 2000Hz (any other value could be chosen just by changing the source code). The "r" factor is the resonance power. If you make r=1, it becomes an oscilator tuned to 2000Hz. If you make r > 1, it becomes exponential and useless.


#!/usr/bin/env python

import wave, struct, math
SAMPLE_RATE = 44100

original = wave.open("NubiaCantaDalva.wav", "r")
filtered = wave.open("NubiaFilteredIIR.wav", "w")
filtered.setnchannels(1)
filtered.setsampwidth(2)
filtered.setframerate(SAMPLE_RATE)

# IIR filter coefficients
freq = 2000 # Hz
r = 0.98
a1 = -2.0 * r * math.cos(freq / (SAMPLE_RATE / 2.0) * math.pi)
a2 = r * r
filter = [a1, a2]
print filter

n = original.getnframes()
original = struct.unpack('%dh' % n, original.readframes(n))
original = [s / 2.0**15 for s in original]

result = [ 0 for i in range(0, len(filter)) ]
biggest = 1
for sample in original:
for cpos in range(0, len(filter)):
sample -= result[len(result) - 1 - cpos] * filter[cpos]
result.append(sample)
biggest = max(abs(sample), biggest)

result = [ sample / biggest for sample in result ]
result = [ int(sample * (2.0**15 - 1)) for sample in result ]
filtered.writeframes(struct.pack('%dh' % len(result), *result))


You can see that our IIR filter can be directly derived from frequency and resonance factor, and it has only two weights (a1 and a2), so it needs only 2 multiplications per sample, while in FIR we needed dozens, or hundreds.

Another thing you may notice is the "biggest" variable. IIR filters are quite the opposite of FIR filters; they can only increase frequency response in a certain range, while FIR can only decrease it. So, in IIR filter, we let it increase (a lot) the response around 2000Hz (above the clipping limits) and then scale all values back to avoid clipping. The net effect is of course damp all frequencies except those around 2000Hz.

The IIR filter coefficients are NOT the impulse response like it were in FIR; so you can NOT find the filter weights using an inverse FFT as we did in FIR. The impulse response of an IIR filter can be found by FFT, but this is not directly useful for actual time-domain processing.

In fact, IIR filters are VERY difficult to calculate and prone to instability problems due to feedback. Since at least the two-order IIR filter is easy to make, as we did above, there are methods to make higher-order IIR filters as a bunch of two-order IIR sub-filters, connected in series or parallel.

Another way (that I have heard of) to design IIR filters is by mimicking some well-known analog filter design. In fact, all analog filters have "infinite response" and rely on feedback (even though analog circuits have continous output), so they match IIR features well.

All software-defined filters I have seen are based on FIR. Maybe embedded things like guitar pedals are using IIR, but I can't tell. Since I don't possess the needed math to realize more complex IIR filters, and the FFT->FIR weights method worked so well, I'd go FIR if I needed to design a filter right now...

2010/01/17

A naïve lowpass FIR filter

Resuming from last post about DFT audio filter, I mentioned that there were two other major methods do create a digital filter: FIR and IIR. This time I will demonstrate a FIR (Finite Impulse Response) filter.

First, we need to resort to some math. We have that operation called CONVOLUTION, which can be understood as a kind of "graphical multiplication", or as a moving, weighted average. Consider the following equation:


average stock index(n) = index(n)*0.5 + index(n-1)*0.3 + index(n-2)*0.2


where "n" is a discrete time position (like year, month, day, whatever). So we are saying that our "average" stock market index is the weighted average of the last three indexes. The weights are [0.5, 0.3, 0,2].

If we trace a graph of these averages over a time interval, we can say that this graph is the convolution of two sequences: the stock index history itself (which is "infinitely" long) versus the list of weights (which has length=3). Of course, drawing an average graph takes a lot of multiplications -- 3 per point, so it is quite intensive.

Moving averages are useful to "smooth out" a curve, avoiding the sharp oscilations of the original signal. In the case of stock market, I may want to devise a "trend line", which is easier to see in a moving average curve than in the sizzly instantaneous market curve. Moving averages make natural lowpass filters; and that's exactly what we are going to do here.

On top of that, there is a very powerful relationship between time-domain and frequency-domain regarding convolutions. Converting a signal from time domain to frequency domain has the power to "convert" a convolution to a simple multiplication, and vice-versa. This means that, if we can define a filter in frequency domain in terms of a multiplication, we can define the same filter in terms of a convolution, which happens in time domain.

Looking at DFT filter code, we have a section where we define a frequency "mask", which we multiply with actual song's spectral distribution, in order to make a bandpass filter:


mask = []
for f in range(0, FFT_LENGTH / 2 + 1):
rampdown = 1.0
if f > LOWPASS:
rampdown = 0.0
elif f < HIGHPASS:
rampdown = 0.0
mask.append(rampdown)


So, in principle, if we convert this frequency mask to a WAV file, and convolve this with our song, we achieve the same filtering result (save for rounding and quantization errors).

This WAV file generated from the filtering mask is NOT an audible wave, it is called the "impulse response". We write it to a WAV file just to demonstrate that we don't need FFT anymore to do filtering. The impulse response of a 3000Hz low-pass filter is something like this:



All we have to do is to convolve an actual audio with this impulse response, and the filtering will happen. That's the code:


#!/usr/bin/env python

import wave, struct, numpy
SAMPLE_RATE = 44100

original = wave.open("NubiaCantaDalva.wav", "r")
filter = wave.open("fir_filter.wav", "r")
filtered = wave.open("NubiaFilteredFIR.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]

n = filter.getnframes()
filter = struct.unpack('%di' % n, filter.readframes(n))
filter = [s / 2.0**31 for s in filter]

'''
result = []
# original content is prepended with zeros to simplify convolution logic
original = [0 for i in range(0, len(filter)-1)] + original

for pos in range(len(filter)-1, len(original)):
# convolution of original * filter
# i.e. a weighted average of len(filter) past samples
sample = 0
for cpos in range(0, len(filter)):
sample += original[pos - cpos] * filter[cpos]
result.append(sample)
'''

result = numpy.convolve(original, filter)

result = [ sample * 2.0**15 for sample in result ]
filtered.writeframes(struct.pack('%dh' % len(result), *result))


Note that there is a section of the program which is commented out. It implements convolution "manually" using two loops and basic arithmetic. This works perfectly well, and I left it (commented) in code just to make clear that convolution is matematically simple. But it is terribly slow; using numpy.convolve() is much, much faster. Actually, this FIR filter is faster than FFT version.

Of course, the FIR filter is fast but we need the impulse response calculated in advance. The following code generates this, and it needs to be run only once (or when you want to change the cut-off frequency of the filter):


#!/usr/bin/env python

LOWPASS = 3000 # Hz
SAMPLE_RATE = 44100 # Hz

import wave, struct, math
from numpy import fft
FFT_LENGTH = 512

NYQUIST_RATE = SAMPLE_RATE / 2.0
LOWPASS /= (NYQUIST_RATE / (FFT_LENGTH / 2.0))

# Builds filter mask. Note that this sharp-cut filter is BAD
mask = []
negatives = []
l = FFT_LENGTH / 2
for f in range(0, l+1):
rampdown = 1.0
if f > LOWPASS:
rampdown = 0.0
mask.append(rampdown)
if f > 0 and f < l:
negatives.append(rampdown)

negatives.reverse()
mask = mask + negatives

fir = wave.open("fir_filter.wav", "w")
fir.setnchannels(1)
fir.setsampwidth(4)
fir.setframerate(SAMPLE_RATE)

# Convert filter from frequency domain to time domain
impulse_response = fft.ifft(mask).real.tolist()

# swap left and right sides
left = impulse_response[0:FFT_LENGTH / 2]
right = impulse_response[FFT_LENGTH / 2:]
impulse_response = right + left

# write in a normal WAV file
impulse_response = [ sample * 2**31 for sample in impulse_response ]
fir.writeframes(struct.pack('%di' % len(impulse_response),
*impulse_response))


Despite the simplicity, a FIR filter is not always faster than a FFT filter. The break-even is around 64 points; more than this, and FFT cheaper. Of course, implementations may change the break-even point: my 512-point FIR filter written in Python is still much faster than the FFT version, so it pays off to use FIR, in particular if my filter must operate in real-time.

The FIR filter is less powerful, in the sense that it can not do every trick that FFT can. If I do some kind of audio morphing in frequency-domain that can not be expressed in terms of a multiplication, it can not be converted to a time-domain convolution, and so it is not realizable as a FIR filter. FIR is a simple tool for a narrow range of jobs, while FFT is a Swiss-army knife, so it is coward to compare them this way.

2010/01/16

DFT audio filter

Ok, in my last post I had promised a digital filter, so we do not depend on Audacity filters anymore.

AFAIK there are three main ways to design a digital filter, like a lowpass filter that we needed in AM modulation. The most common is a FIR filter. Another way is an IIR filter. And a third way is using Discrete Fourier Transform (DFT). In this post, I will use the easiest way, which is DFT. Some post in the future will show a FIR version.

DFT can be implemented by a fast algorithm called FFT (Fast Fourier Transform), and most mathematical libraries have a FFT implementation. Python-Numeric is no exception.

DFT converts a time-domain signal, like an audio wave, to a frequency-domain representation. For example, the "spectrum analyzer" feature found in most PC MP3 players and some mini-systems is (or could be) a direct result of DFT calculation:



Apart from showing this fancy spectrum analyzer, DFT has countless other applications.

DFT is not an one-way signal processing feature. Given a frequency domain representation, we can calculate the IDFT (Inverse DFT), to get back the time-domain wave. So, we can get some audio, transform into DFT, manipulate the frequencies (e.g. increasing bass, decreasing midrange, those equalizer things) and then convert back into time-domain.

In our case, we want to build a lowpass/highpass filter, so we just have to zero out the frequencies we don't want, in frequency domain. Such sharp cutting is NOT a good filter design; it causes distortions. But it is ok for a simple demonstration.

Converting to/from frequency domain seems too much work, and in fact there are ways to do filtering without this. FIR filters operate directly on time-domain samples, so they are normally the choice for practical filter applications. But they are not so easy to understand, so this will have to wait for a next post.

For this post, I used a female singer's voice that I like much: Núbia Lafayette. Another reason I used this particular sample is that I own the CD, so nobody can complain about piracy etc. Let's see the original audio:

NubiaCantaDalva.wav

As usual, if your browser does not understand EMBED tags, you can get the file directly from http://epx.com.br/wav.

Now, the filtered version (1000-4000Hz bandpass filter), which sounds like a very small radio receiver:

NubiaFiltered.wav

And the code which does the trick:


#!/usr/bin/env python

LOWPASS = 4000 # Hz
HIGHPASS = 1000 # Hz
SAMPLE_RATE = 44100 # Hz

import wave, struct, math
from numpy import fft
FFT_LENGTH = 2048

# The higher the better, but this seems enough
OVERLAP = 512

FFT_SAMPLE = FFT_LENGTH - OVERLAP

# Nyquist rate = bandwidth available for a given sample rate
NYQUIST_RATE = SAMPLE_RATE / 2.0

# Convert frequencies from Hz to our digital sampling units
LOWPASS /= (NYQUIST_RATE / (FFT_LENGTH / 2.0))
HIGHPASS /= (NYQUIST_RATE / (FFT_LENGTH / 2.0))

zeros = [ 0 for x in range(0, OVERLAP) ]

# Builds filter mask. Note that this filter is BAD, a
# good filter must have a smooth curve, respecting a
# dB/octave attenuation ramp!
mask = []
for f in range(0, FFT_LENGTH / 2 + 1):
rampdown = 1.0
if f > LOWPASS:
rampdown = 0.0
elif f < HIGHPASS:
rampdown = 0.0
mask.append(rampdown)

def bound(sample):
# takes care of "burnt" samples, due some mistake in filter design
if sample > 1.0:
print "!",
sample = 1.0
elif sample < -1.0:
print "!",
sample = -1.0
return sample

original = wave.open("NubiaCantaDalva.wav", "r")
filtered = wave.open("NubiaFiltered.wav", "w")
filtered.setnchannels(1)
filtered.setsampwidth(2)
filtered.setframerate(SAMPLE_RATE)

n = original.getnframes()
original = struct.unpack('%dh' % n, original.readframes(n))
# scale from 16-bit signed WAV to float
original = [s / 32768.0 for s in original]

saved_td = zeros

for pos in range(0, len(original), FFT_SAMPLE):
time_sample = original[pos : pos + FFT_LENGTH]

# convert frame to frequency domain representation
frequency_domain = fft.fft(time_sample, FFT_LENGTH)
l = len(frequency_domain)

# mask positive frequencies (f[0] is DC component)
for f in range(0, l/2+1):
frequency_domain[f] *= mask[f]

# mask negative frequencies
# http://stackoverflow.com/questions/933088/clipping-fft-matrix
for f in range(l-1, l/2, -1):
cf = l - f
frequency_domain[f] *= mask[cf]

# convert frame back to time domain
time_domain = fft.ifft(frequency_domain)

# modified overlap-add logic: previously saved samples
# prevail in the beginning, and they are ramped down
# in favor of this frame's samples, to avoid 'clicks'
# in either end of frame.
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)

# reserve last samples for the next frame
saved_td = time_domain[FFT_SAMPLE:]
# do not write reserved samples right now
time_domain = time_domain[:FFT_SAMPLE]

# scale back into WAV 16-bit and write
time_domain = [ bound(sample) * 32767 for sample in time_domain ]
filtered.writeframes(struct.pack('%dh' % len(time_domain),
*time_domain))


There is one twist in this code that is worth mentioning. Since any filtering you do in frequency domain will cause phase distortion, you can't just piggyback the processed audio frames one after another, because the audio wave will not transition smoothly from one frame to the next. It will have a different phase at the next frame, so there will be a sharp discontinuity, very annoying to the ears. (If you don't believe me, you can just set OVERLAP to zero and see what happens.)

Both textbook solutions for this (overlap-save and overlap-add) seem not to solve this problem. I have modified the overlap-add method, which apparently solved the issue. My solution fades in the current frame while fading out the end of previous frame. Maybe there is a better solution for this, but I didn't find any.

Another thing that must be remembered by anyone that wants to play with FFT, is that frequency representation is linear. We are used to logarithmic spectrum analyzers, which match the octaves in music, but FFT is different. This means that frequency resolution of a FFT-generated array is much more precise at low octaves than at high octaves.

FFT generates two frequency "lobes": positive and negative frequencies. This sounds very strange, because there can't be negative frequencies in physical world, but DFT has this mathematical feature. In our filter, we had two choices: just zero out negative frequencies (which reduces the final audio volume by half), or filter them in the same fashion as positive frequencies, as we do.

The array generated by FFT consists of complex numbers. This sounds scary but it has a valid purpose: a complex number can represent amplitude AND phase in a single shot. For example, if we find 1+0j in the 3000Hz 'bar', it means that audio has a 3000Hz component with amplitiude 1.0 and in phase with cos(x). If the sample were 0.707+0.707j, it means that the 3000Hz component still has amplitude = 1 (the modulus) but it is 45 degrees ahead of cos(x).

Some applications, for example the spectrum analyzer, do not care about signal phase, so they could employ DCT (discrete cosine transform) whose output is an array of real numbers. The original waveform can be obtained by IDCT, but of course the reconstructed signal will be phase-distorted.

2010/01/13

Siemens SpeedStream 4200 x Thomson SpeedTouch 510v6

A quem interessar possa, o SpeedStream 4200 funciona melhor aqui, com menos perda de pacotes, apesar do SpeedTouch reportar um SNR mais alto. Além disso a tela de diagnósticos do SpeedStream é bem melhor.

Considerando que paguei 15 reais nesse Thomson, também não dá pra reclamar muito :)

2010/01/12

AM modulation, illustrated

Radio is the one thing I'd like to play with, but I have not. For several reasons, this interest of mine always end up buried deep at in-tray, and perhaps I'll never fiddle with it, after all.

For a signal to travel over radio waves, it must be modulated first, which means having it transposed to a frequency bandwidth and format suitable to the transmission medium. Modulation techniques are closely related to telecommunications and information theory, and it is possible to play with modulation within the limits of a computer. This is a theme that I must confess I don't understand much, but I am mesmerized by it nevertheless. And to be honest, I find incredible how a couple simple calculations can make the miracle.

In this post, I am going to illustrate the AM modulation techniques, using WAV audio files. To begin with, we need a baseband signal, that is, some audio that fills all available bandwidth from 0Hz. I have recorded my own, ugly voice in order to torture your ears:

(paralelepipedo.wav)

By the way, I am using the EMBED tag to put all audios on page. If it does not work in your browser, you can get the audios at http://epx.com.br/wav.

AM modulation demands that signal is restricted to a bandwidth, so the modulated signal will also "fit" in a predetermined frequency band. So, we need to apply a low-pass filter on my voice, and we define the cut-off at 1000Hz. The result is my voice in lower quality.

(paralelepipedo_lopass.wav)

I have used an Audacity's low-pass filter plug-in. For the next post, I promise to write a filter from scratch, in Python. These Audacity filters that I am using are not perfect, but are enough to illustrate our points.

Amplitude modulation is just multiplying the original signal by a sinusoidal wave, the carrier. In this case, I have chosen a 3000Hz carrier, well above the cut-off of original signal, which is 1000Hz. The carrier noise itself is:

(carrier3000.wav)

Modulation result (voice multiplied by whistle) is:

(amsc.wav)

Note that it is almost impossible to understand the speech. To be exact, this modulation is named AM-SC (Supressed Carrier), for a reason that I will clarify a bit later.

Modulation makes my voice to exchange band from 0-1000 to 2000-4000Hz. Actually, AM modulation creates two "copies" of the original signal, one at 2000-3000Hz band and another at 3000-4000Hz. So, normal AM "wastes" bandwidth because modulated signal occupies twice as much of bandwidth.

Nevertheless, AM is still quite useful, because I can "move" my voice to frequencies suitable for electromagnetic transmission. And, even more important, modulation allows allocation of multiple signals on the same medium, each one occupying a different band, and each one can be recovered separately.

The "normal", old radio AM modulation sounds like this:

(am.wav)

In this modulation, we can hear the carrier, while we could not in AM-SC. The following Python program was employed to generate the modulated AM and AM-SC audios:


#!/usr/bin/env python

import wave, struct, math

baseband = wave.open("paralelepipedo_lopass.wav", "r")

amsc = wave.open("amsc.wav", "w")
am = wave.open("am.wav", "w")
carrier = wave.open("carrier3000.wav", "w")

for f in [am, amsc, carrier]:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(44100)

for n in range(0, baseband.getnframes()):
base = struct.unpack('h', baseband.readframes(1))[0] / 32768.0
carrier_sample = math.cos(3000.0 * (n / 44100.0) * math.pi * 2)

signal_am = signal_amsc = base * carrier_sample
signal_am += carrier_sample
signal_am /= 2

amsc.writeframes(struct.pack('h', signal_amsc * 32767))
am.writeframes(struct.pack('h', signal_am * 32767))
carrier.writeframes(struct.pack('h', carrier_sample * 32767))


AM-SC is simply the original signal (whose band has been restricted by a lowpass filter) multiplied by the carrier, which is simply a sinusoidal wave.

AM modulation is the same as AM-SC, except that carrier is added afterwards. This allows the AM receiver to be much simpler, as we will see.

Well, it's time to proof that we can extract the original voice from these modulated audios. The AM-SC modulation is as simple as multiply the signal again by the same carrier, and then passing the result through a lowpass filter. There are the demodulated audios, before and after lowpass:

(demod_amsc_ok.wav)

(demod_amsc_ok_lopass.wav)

As said, AM-SC demodulation is almost equal to modulation. Multiplying a signal twice by the same carrier throws the voice back to its original band (0-1000Hz), and creates a new copy around 6000Hz which needs to be suppresed by the lowpass filter.

This is the AM-SC demodulator code:


modulated = wave.open("amsc.wav", "r")

demod_amsc_ok = wave.open("demod_amsc_ok.wav", "w")
demod_amsc_nok = wave.open("demod_amsc_nok.wav", "w")
demod_amsc_nok2 = wave.open("demod_amsc_nok2.wav", "w")

for f in [demod_amsc_ok, demod_amsc_nok, demod_amsc_nok2]:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(44100)

for n in range(0, modulated.getnframes()):
signal = struct.unpack('h', modulated.readframes(1))[0] / 32768.0
carrier = math.cos(3000.0 * (n / 44100.0) * math.pi * 2)
carrier_phased = math.sin(3000.0 * (n / 44100.0) * math.pi * 2)
carrier_freq = math.cos(3100.0 * (n / 44100.0) * math.pi * 2)

base = signal * carrier
base_nok = signal * carrier_phased
base_nok2 = signal * carrier_freq

demod_amsc_ok.writeframes(struct.pack('h', base * 32767))
demod_amsc_nok.writeframes(struct.pack('h', base_nok * 32767))
demod_amsc_nok2.writeframes(struct.pack('h', base_nok2 * 32767))


One little big problem of AM-SC demodulation: the carrier wave generated at receiver must be EXACTLY equal to transmitter, both in frequency and phase. Any difference will wreck the demodulation. In the code above, I simulated two demodulation problems: carrier with wrong phase (90 degrees behind), and with wrong frequency (100Hz higher). The resulting demodulations are very bad, as you can hear below (already lowpassed):

(demod_amsc_nok_lopass.wav)

(demod_amsc_nok2_lopass.wav)

It is very difficult to generate a local carrier at receiver with these constraints. It would be easier in "old radio" AM because the carrier is sent together, so it could be extracted directly from the antenna signal. Due to this, AM-SC is seldom employed in practice. (*)

Now, let's see the "old radio" AM demodulator:


modulated = wave.open("am.wav", "r")

f = demod_am = wave.open("demod_am.wav", "w")
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(44100)

for n in range(0, modulated.getnframes()):
signal = struct.unpack('h', modulated.readframes(1))[0] / 32768.0
signal = abs(signal)
demod_am.writeframes(struct.pack('h', signal * 32767))


Note how simpler it is, the absence of transcendental functions. AM demodulator is simply a detector, which cuts off the negative part of wave. That's why you can build a galena radio with just a diode and a capacitor, the first being the abs() function and the latter being the lowpass filter.

There are the demodulation results, before and after the lowpass filter:

(demod_am.wav)

(demod_am_lopass.wav)

The remaining noise is likely due to the abs()-based detector in demodulation; In a real-world AM radio, the noise generated by detector would be of a far higher frequency, and lowpass filter would remove it completely. (Remember that we have modulated to a very low frequency, 3000Hz).

Both AM and AM-SC modulations waste bandwidth, because they generate two "copies" of the same content around carrier. Moreover, AM sends the carrier all along with content. We can say that an AM transmitter spends 75% of power just sending "garbage" which does not increase range -- it is there just to help receivers (an heritage from galena days).

But there is a way to improve AM efficiency. Since AM-SC signal has two copies, we can just filter out one of them, sending only the other. This is called AM-SSB (supressed side band). This is how AM-SSB sounds like (lower band removed, only upper band is kept):

(amssb.wav)

Not much different from AM-SC; and in fact the AM-SC demodulator can cope with AM-SSB signal without any changes:

(demod_amssb_ok.wav)

(demod_amssb_ok_lopass.wav)

Besides saving power by sending only one copy of original content, without the carrier, AM-SSB has a second, definite advantage over AM-SC. See what happens when we demodulate AM-SSB with an out-of-phase carrier:

(demod_amssb_nok_lopass.wav)

The voice has been recovered perfectly. So, an AM-SSB receiver does not need to worry with carrier's phase. Only the frequency is important, and it must be watched closely; any frequency deviation causes audio distortion pretty much like AM-SC:

(demod_amssb_nok2_lopass.wav)

AM-SSB is the standard modulation for ham radio, police, aviation, analog TV video etc. And the same technique is employed in FDM multiplexers and countless other devices that need to fit many signals in a single path. Removal of the unneeded sideband in tranmission can be done with an analog filter, but such filter is difficult and expensive to build. DSP-powered radios use a technique called Hilbert Transform to generate the AM-SSB signal directly, without the need of a filter.

(*) A surprising usage of AM-SC is the FM stereo. The second audio channel is encoded within the audio signal, with a 38KHz carrier. A 19KHz pilot tone is transmitted all the time so the receiver can generate a local carrier with perfect frequency and phase. Of couse this implies that FM audio can not be more treble than 19KHz.

Modulação AM ilustrada

Rádio é uma coisa com a qual eu gostaria de ter mexido, mas ainda não o fiz. Por diversas razões, este interesse acaba sempre em segundo plano frente a coisas mais urgentes, e quem sabe eu nunca brinque com rádio mesmo.

Para um sinal viajar nas ondas de rádio, ele tem de ser modulado, ou seja, transposto para uma freqüência e formato adequados à transmissão. As técnicas de modulação do rádio têm parentesco próximo com informática e telecomunicações, É possível "brincar" com modulação no computador mesmo. É um tema do qual eu confessadamente não entendo muito, mas acho fascinante. E para ser honesto, acho incrível que técnicas aparentemente simples façam o milagre de modular e demodular um sinal.

Neste post, vou ilustrar as técnicas de modulação AM. Quem tiver interesse em aprofundar-se no tema, tente achar o livro "Modem e transmissão de dados", de Fábio de Azevedo Montoro, editora Érica. Apesar de velho, é um dos poucos livros técnicos escritos por brasileiro que presta.

Bem, vamos começar com um sinal de banda-base, ou seja, que ocupa toda a banda disponível a partir de 0Hz. Uma gravação da minha própria voz, pra sacanear com os ouvidos de todo mundo:

(paralelepipedo.wav)

A propósito, estou usando a tag EMBED para colocar o áudio na página. Se não funcionar, você pode pescar os arquivos em http://epx.com.br/wav.

A modulação AM exige que o sinal original ocupe uma banda limitada, de modo que o sinal modulado também "caiba" dentro da banda desejada. Assim, precisamos pegar o sinal original e passar num filtro passa-baixas, até 1000Hz. O resultado é a mesma voz um pouco mais abafada:

(paralelepipedo_lopass.wav)

Para o próximo post, prometo escrever um filtro em Python. Desta vez bateu a preguiça e usei os recursos do Audacity mesmo para fazer a filtragem. Não é perfeito mas é suficiente para nossos fins.

A modulação AM consiste simplesmente em multiplicar o sinal original por uma onda senoidal, a portadora. No caso, escolhi uma portadora de 3000Hz, bem distante da banda ocupada pela minha voz filtrada (1000Hz). O ruído da portadora em si é:

(carrier3000.wav)

O resultado da modulação (voz x apito) é:

(amsc.wav)

Note que a voz e as vogais ficaram agudas e quase irreconhecíveis. A rigor, esta modulação denomina-se AM-SC (AM com portadora suprimida), por razão que logo esclareço.

A modulação faz minha voz mudar de banda: ela passa a ocupar uma faixa de 2000 a 4000Hz. Na verdade, a modulação AM produz duas "cópias" do sinal original: uma ocupando a banda 2000-3000Hz, e outra ocupando a banda 3000-4000Hz. A modulação AM normal "desperdiça" banda pois o sinal modulado ocupa o dobro de espaço que o original.

Mesmo assim, AM é muito útil, pois obviamente eu consigo "mover" a minha voz para qualquer faixa de freqüências que seja conveniente à transmissão. Mais importante que isso: a modulação permite que diversos sinais ocupem o mesmo meio, bastando que cada sinal tenha uma portadora diferente e as bandas não se sobreponham.

Podemos modular em AM "normal":

(am.wav)

Note que o apito da portadora pode ser claramente ouvido. Esta é a modulação utilizada pela rádio AM tradicional.

Segue o programa Python que gerou as modulações AM-SC e AM "tradicional":


#!/usr/bin/env python

import wave, struct, math

baseband = wave.open("paralelepipedo_lopass.wav", "r")

amsc = wave.open("amsc.wav", "w")
am = wave.open("am.wav", "w")
carrier = wave.open("carrier3000.wav", "w")

for f in [am, amsc, carrier]:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(44100)

for n in range(0, baseband.getnframes()):
base = struct.unpack('h', baseband.readframes(1))[0] / 32768.0
carrier_sample = math.cos(3000.0 * (n / 44100.0) * math.pi * 2)

signal_am = signal_amsc = base * carrier_sample
signal_am += carrier_sample
signal_am /= 2

amsc.writeframes(struct.pack('h', signal_amsc * 32767))
am.writeframes(struct.pack('h', signal_am * 32767))
carrier.writeframes(struct.pack('h', carrier_sample * 32767))


A modulação AM-SC é simplesmente a multiplicação do sinal original, cuja banda é previamente confinada numa faixa, por uma portadora, que é nada mais que uma onda senoidal.

A modulação AM tradicional é a mesma coisa, exceto que a portadora é adicionada ao sinal modulado, depois da multiplicação. Isto permite que o receptor seja consideravelmente mais simples, conforme veremos.

Bem, agora tá na hora de demodular, ou seja, tentar recuperar o sinal original, usando os arquivos WAV modulados. A demodulação AM-SC consiste em nova multiplicação pela portadora, seguida de um filtro passa-baixas. Segue os resultados, antes e depois do filtro:

(demod_amsc_ok.wav)

(demod_amsc_ok_lopass.wav)

A demodulação AM-SC é essencialmente igual à modulação. O efeito de multiplicar duas vezes pela mesma portadora joga de volta o sinal para sua banda original (0-1000Hz), criando uma nova cópia indesejável em torno de 6000Hz, que removemos com o filtro.

Segue o código do demodulador AM-SC:


modulated = wave.open("amsc.wav", "r")

demod_amsc_ok = wave.open("demod_amsc_ok.wav", "w")
demod_amsc_nok = wave.open("demod_amsc_nok.wav", "w")
demod_amsc_nok2 = wave.open("demod_amsc_nok2.wav", "w")

for f in [demod_amsc_ok, demod_amsc_nok, demod_amsc_nok2]:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(44100)

for n in range(0, modulated.getnframes()):
signal = struct.unpack('h', modulated.readframes(1))[0] / 32768.0
carrier = math.cos(3000.0 * (n / 44100.0) * math.pi * 2)
carrier_phased = math.sin(3000.0 * (n / 44100.0) * math.pi * 2)
carrier_freq = math.cos(3100.0 * (n / 44100.0) * math.pi * 2)

base = signal * carrier
base_nok = signal * carrier_phased
base_nok2 = signal * carrier_freq

demod_amsc_ok.writeframes(struct.pack('h', base * 32767))
demod_amsc_nok.writeframes(struct.pack('h', base_nok * 32767))
demod_amsc_nok2.writeframes(struct.pack('h', base_nok2 * 32767))


Um problema da demodulação AM-SC é que a portadora do receptor tem de ser EXATAMENTE igual à do transmissor. Qualquer diferença em fase ou frequência causa distorções graves no sinal. No código acima, procurei simular problemas com moduladora fora de fase (usando seno em vez de cosseno) e com frequência errada (3100Hz em vez de 3000). Os resultados desastrosos podem ser ouvidos abaixo, já filtrados pelo passa-baixas:

(demod_amsc_nok_lopass.wav)

(demod_amsc_nok2_lopass.wav)

Esta necessidade de manter uma portadora precisa no receptor, sem que haja muitos meios de recuperá-la a partir do sinal modulado, acaba tornando o AM-SC uma modulação pouco prática. Um dos poucos usos reais é a trasnsmissão estéro FM, onde o segundo canal é embutido dentro do próprio áudio, com portadora em 38KHz e sinal-piloto transmitido o tempo todo para garantir a perfeita demodulação no receptor.

Vejamos agora o código do demodulador AM tradicional:


modulated = wave.open("am.wav", "r")

f = demod_am = wave.open("demod_am.wav", "w")
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(44100)

for n in range(0, modulated.getnframes()):
signal = struct.unpack('h', modulated.readframes(1))[0] / 32768.0
signal = abs(signal)
demod_am.writeframes(struct.pack('h', signal * 32767))


Note que este demodulador nem usou seno ou cosseno. Ele é meramente um detector, ou seja, transforma sinais negativos em positivos, o que num rádio real seria feito por um diodo ou ponte de diodos. É isto que permite um receptor AM galena funcionar: basta um diodo e um capacitor.

Segue o resultado antes e depois do filtro passa-baixas:

(demod_am.wav)

(demod_am_lopass.wav)

Ficou um ruidozinho de portadora mesmo na versão filtrada, mas isto se deve muito à filtragem ruim (seria mais fácil filtrar se a freqüencia da portadora fosse muito mais alta que a do sinal original, como é o caso da transmissão de rádio).

Tanto a modulação AM-SC quanto AM tem a séria desvantagem de desperdiçar banda, pois geram duas "cópias" do mesmo sinal ao redor da portadora. Além disso, AM transmite a portadora o tempo todo junto com as cópias. No fim das contas, um transmissor AM gasta 75% da energia transmitindo "bobagens", que não aumentam seu alcance, servem apenas para facilitar o trabalho do receptor.

Mas existe uma forma de resolver isto. Já que o sinal AM-SC possui duas cópias, podemos filtrar uma fora, transmitindo apenas a outra. Isto denomina-se AM-SSB (AM com banda suprimida). Segue o sinal AM-SSB com a cópia inferior (2000-3000Hz) filtrada:

(amssb.wav)

Não parece muito diferente do sinal AM-SC, e de fato o mesmo demodulador AM-SC consegue recuperar a voz original a partir do sinal AM-SSB:

(demod_amssb_ok.wav)

(demod_amssb_ok_lopass.wav)

Além de não desperdiçar nenhuma energia (apenas uma cópia do sinal é transmitida, sem a portadora), o AM-SSB tem uma outra vantagem, ainda mais surpreendente. Veja o que acontece com a voz demodulada com uma portadora fora de fase:

(demod_amssb_nok_lopass.wav)

A voz foi recuperada normalmente. Desta forma, o receptor AM-SSB não precisa se preocupar com a fase da portadora gerada localmente, o que facilita muito as coisas. A freqüência ainda é importannte, qualquer desvio causa a mesma distorção que a observada em AM-SC:

(demod_amssb_nok2_lopass.wav)

AM-SSB é a modulação padrão utilizada pela radiocomunicação em geral (radioamador, polícia, aviação, etc. etc.). Além disso, a mesma técnica básica é utilizada em multiplexadores FDM, no sinal de vídeo da TV analógica, e na transmissão do canal estéreo das rádios FM (o segundo canal é modulado dentro do áudio, com portadora de 38KHz e largura de 19KHz).

A remoção da cópia excedente no transmissor pode ser feita com um filtro analógico, embora seja difícil e caro construir o filtro com as características necessárias. Rádios com processamento digital de sinais podem utilizar uma técnica denominada Transformada de Hilbert, o que dispensa este componente.