Skip to content

Lab 12: Adding Waves — Superposition and Beats

Time: ~40 minutes | Prerequisites: Lab 11 | Hardware: Pico 2 (no microphone needed)

Real sound is never one tone

Echo waving welcome A voice, a violin, a car engine — none of them is a single sine wave. They're sums of sine waves, added up. Which sets up the question the rest of this course answers: if sounds are sums, can we work out what went into the mix? Let's tune in.

What You'll Build

Complex waveforms built by stacking simple ones: an octave pair, a square wave assembled from harmonics, perfect cancellation, and beats — the wobble two nearly-identical tones make.

Learning Objectives

  • Apply superposition: add waves sample by sample
  • Explain constructive and destructive interference
  • Build a square wave from odd harmonics
  • Predict the beat frequency of two close tones
  • Connect harmonic content to timbre

Concepts Introduced

ID Concept
310 Superposition Principle
311 Wave Addition
312 Constructive Interference
313 Destructive Interference
314 Beat Frequency
315 Amplitude Envelope
316 Fundamental Frequency
317 Overtones
318 Timbre
319 Additive Synthesis

Background

Superposition: just add them

When two sounds arrive together, the air pressure at your ear is simply the sum of what each would produce alone.

1
combined = [a[i] + b[i] for i in range(len(a))]

No special rule, no interaction. Waves pass through each other unchanged.

Harmonics and why a violin isn't a flute

Play the same note on different instruments and the fundamental frequency is identical — that's why it's the same note. What differs is the overtones: multiples of the fundamental, each at its own strength.

That recipe of overtones is timbre. It's why you can recognise a friend's voice from one word.

Building a square from sines

Add odd harmonics at shrinking amplitudes:

1
sin(f) + sin(3f)/3 + sin(5f)/5 + sin(7f)/7 + ...

The corners get squarer with every term. A perfect square wave needs infinitely many — which is also why a clipped signal (Lab 10) contains frequencies that were never in the room. Flatten the tops of a wave and you've added harmonics.

This is the FFT question, backwards

Echo thinking Here you add known sines to make a complex wave. The FFT does the reverse: it takes a complex wave and tells you which sines were added. Same relationship, opposite direction — and the reverse is far more useful, because the world hands you the sum and keeps the recipe secret.

Procedure

Step 1 — Run it

Open 12-superposition.py and run it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# Lab 12: Adding Waves -- Superposition and Beats
#
# Real sound is never one pure tone. A voice, a guitar string, a car engine --
# all of them are many sine waves stacked on top of each other, added together
# sample by sample.
#
# That is the whole reason the FFT matters. If sounds are SUMS of sine waves,
# then "what is in this sound?" becomes "which sine waves were added, and how
# much of each?" Answering that question is what we spend the next four labs
# learning to do.

import config
import math

RATE = config.SAMPLE_RATE
N = 64
BASE = RATE / N              # 200 Hz -- one cycle across the window


def sine(freq, amp=1.0, phase=0.0, n=N):
    return [amp * math.sin(2 * math.pi * freq * (i / RATE) + phase)
            for i in range(n)]


def add(*waves):
    """Superposition: at every instant, just add the values."""
    return [sum(w[i] for w in waves) for i in range(len(waves[0]))]


def plot(values, label, width=56):
    print()
    print("--- %s ---" % label)
    peak = max(abs(v) for v in values) or 1.0
    mid = width // 2
    for v in values:
        pos = int(mid + (v / peak) * (mid - 1))
        pos = max(0, min(width - 1, pos))
        line = [" "] * width
        line[mid] = "|"
        line[pos] = "*"
        print("".join(line))


# --- two tones an octave apart ---------------------------------------------
a = sine(BASE)                       # 200 Hz
b = sine(2 * BASE, amp=0.5)          # 400 Hz, half as loud
plot(add(a, b), "200 Hz + 400 Hz (an octave) -- still repeats once")

# --- adding odd harmonics builds a square wave ----------------------------
# This is additive synthesis. Every extra odd harmonic makes the corners
# sharper. A perfect square wave needs infinitely many.
square = add(sine(BASE),
             sine(3 * BASE, amp=1 / 3),
             sine(5 * BASE, amp=1 / 5),
             sine(7 * BASE, amp=1 / 7),
             sine(9 * BASE, amp=1 / 9))
plot(square, "200 + 600 + 1000 + 1400 + 1800 Hz -- becoming a SQUARE")

# --- interference ----------------------------------------------------------
print()
print("=== Interference: same frequency, different phase ===")
same = add(sine(BASE), sine(BASE))
opposite = add(sine(BASE), sine(BASE, phase=math.pi))
print("in phase  (0)  -> peak amplitude %.2f   CONSTRUCTIVE" %
      max(abs(v) for v in same))
print("anti-phase(pi) -> peak amplitude %.2f   DESTRUCTIVE" %
      max(abs(v) for v in opposite))
print("Two identical sounds can cancel to silence. That is how noise-")
print("cancelling headphones work.")

# --- beats -----------------------------------------------------------------
# Two frequencies that are CLOSE but not equal drift in and out of step.
# The wobble rate is the difference between them.
print()
print("=== Beats: two close tones ===")
f1, f2 = 200.0, 205.0
long_n = 2048
beat = [math.sin(2 * math.pi * f1 * (i / RATE)) +
        math.sin(2 * math.pi * f2 * (i / RATE)) for i in range(long_n)]

print("%.0f Hz + %.0f Hz -> you hear a wobble at %.0f Hz" % (f1, f2, abs(f2 - f1)))
print()
print("Envelope over %.0f ms (each row is the local peak):" %
      (long_n / RATE * 1000))
CHUNK = 64
width = 50
for start in range(0, long_n, CHUNK):
    chunk = beat[start:start + CHUNK]
    level = max(abs(v) for v in chunk)
    bar = int(level / 2.0 * width)
    print("  %5.1f ms |%s" % (start / RATE * 1000, "#" * bar))

print()
print("The envelope swells and fades %.0f times a second -- that is the beat." %
      abs(f2 - f1))
print("Piano tuners listen for exactly this, and tighten the string until")
print("the wobble stops.")

Step 2 — Watch a square wave assemble

The second plot adds five odd harmonics. Compare it to the pure sine above: flatter top, steeper sides. Try deleting terms and re-running — with only two harmonics it barely differs from a sine.

Step 3 — Cancel a sound completely

1
2
in phase  (0)  -> peak amplitude 2.00   CONSTRUCTIVE
anti-phase(pi) -> peak amplitude 0.00   DESTRUCTIVE

Two identical waves, half a cycle apart, sum to exactly nothing. That's not a trick — it's how noise-cancelling headphones work: sample the noise, invert it, play it back.

Step 4 — Beats

Add 200 Hz and 205 Hz and the envelope swells and fades 5 times a second — the difference between them:

1
  beat frequency = |f1 - f2|

Look at the envelope plot: it starts full, collapses near 100 ms, and rises again. That's one half-cycle of a 5 Hz wobble.

Piano tuners use this

Echo offering a tip Strike a tuning fork and a piano string together, and count the wobbles. Three per second means you're 3 Hz out. Tighten until the wobble stops and you're exact. It's a frequency comparison accurate to a fraction of a hertz, using only your ears.

Step 5 — Predict, then measure

Prediction: what beat frequency do 440 Hz and 443 Hz produce? What about 440 and 460?

Change f1 and f2 and check.

Expected Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
=== Interference: same frequency, different phase ===
in phase  (0)  -> peak amplitude 2.00   CONSTRUCTIVE
anti-phase(pi) -> peak amplitude 0.00   DESTRUCTIVE

=== Beats: two close tones ===
200 Hz + 205 Hz -> you hear a wobble at 5 Hz

Envelope over 160 ms (each row is the local peak):
    0.0 ms |#################################################
   50.0 ms |###################################
  100.0 ms |###
  155.0 ms |########################################

Troubleshooting

Symptom Likely cause Fix
Cancellation isn't exactly zero Floating-point rounding 1e-16 is zero for our purposes
No visible beat Frequencies too far apart Beats are only audible when close — try within 10 Hz
Envelope looks flat Window too short to hold one beat cycle A 5 Hz beat needs 200 ms; raise long_n
Square wave looks like a sine Too few harmonics Add more odd terms

Challenges

  1. Sawtooth. Use all harmonics (not just odd) at amplitude 1/n. How does it differ from the square?
  2. Beat hunt. Find the smallest frequency difference that still shows a visible envelope in a 160 ms window. What limits it? (This is Lab 23's frequency-resolution problem in disguise.)
  3. Fake an instrument. Pick a fundamental and invent an overtone recipe. Plot it. Would you guess it's a "brass" or "string" sound from the shape alone?

Check Your Understanding

  1. State the superposition principle in one sentence.
  2. Two identical tones cancel completely. What must be true about their phase?
  3. What's the beat frequency of 300 Hz and 307 Hz?
  4. Why do a trumpet and a flute playing the same note sound different?
  5. Lab 10 showed clipping creating new frequencies. Using this lab, explain why.

You can build any sound now

Echo celebrating Adding waves is the easy direction. Next lab we go backwards — given a mixed-up signal, work out which frequencies are inside it. That's the one everything else stands on.


Next: Lab 13: Correlation | Previous: Lab 11