Sound

Sampling, sample rate, bit depth and file size, and finding a note from its samples.

F8.8Data representationGCSE15 min

Do this lesson in the simulator

Sound is a wave: air pressure rising and falling, smoothly and continuously. A computer can only store numbers, so to record sound it measures the wave at regular moments and stores each measurement as a binary number. That is sampling. In this lesson you sample a note, measure how much data it takes, and use the samples to work out which note it was and play it on BugBot's buzzer.

Sampling a wave

A note of 440 hertz is a wave that repeats 440 times a second. Here is one sampled 4,000 times a second, shown for its first few samples:

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

import math

rate = 4000                         # samples per second
freq = 440                          # the note, in hertz
samples = []
for i in range(40):                 # a hundredth of a second
    t = i / rate
    samples.append(math.sin(2 * math.pi * freq * t))

for s in samples[:20]:
    bar = int((s + 1) * 20)
    print(f"{s:6.2f} " + " " * bar + "*")

Run this in the simulator

Each row is one sample, and the stars trace the shape of the wave. The sample rate is how many samples are taken each second, measured in hertz. More samples per second follow the wave more closely.

Bit depth

Each sample is stored as a whole number with a fixed number of bits: the bit depth. With 8 bits there are 256 levels, so each measurement is rounded to the nearest level:

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

import math

def sample(freq, rate, seconds, depth):
    levels = 2 ** depth
    out = []
    for i in range(int(rate * seconds)):
        wave = math.sin(2 * math.pi * freq * i / rate)          # -1 to 1
        out.append(round((wave + 1) / 2 * (levels - 1)))        # 0 to levels - 1
    return out

print("2-bit:", sample(440, 4000, 0.004, 2))
print("8-bit:", sample(440, 4000, 0.004, 8))

Run this in the simulator

With 2 bits there are only four levels, 0 to 3, and the wave becomes a rough staircase. With 8 bits the steps are much finer. A higher bit depth records the sound more accurately.

File size

The size of a sound file in bits is:

sample rate × bit depth × duration in seconds

For stereo, multiply by the 2 channels as well.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

def sound_bits(rate, depth, seconds, channels=1):
    return rate * depth * seconds * channels

print("robot beep, 8 kHz, 8-bit, 1 s:", sound_bits(8000, 8, 1) / 8, "bytes")
print("CD quality, 44.1 kHz, 16-bit, stereo, 3 minutes:", sound_bits(44100, 16, 180, 2) / 8 / 1000 / 1000, "MB")

Run this in the simulator

A three-minute song at CD quality is about 32 MB, which is why music is compressed.

From samples back to a note

The samples hold the wave, so the note can be recovered from them. Count how often the wave crosses the middle going upwards: that is once per cycle, so crossings per second is the frequency.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

import math

rate, seconds = 8000, 0.5
samples = [math.sin(2 * math.pi * 523 * i / rate) for i in range(int(rate * seconds))]

rises = 0
for i in range(1, len(samples)):
    if samples[i - 1] < 0 <= samples[i]:
        rises = rises + 1
freq = round(rises / seconds)
print("the samples hold a note of about", freq, "Hz")
tone(freq, 0.5)

Run this in the simulator

Quality and size

Increasing the sample rate or the bit depth makes the recording closer to the original sound, and makes the file bigger. Every recording is a choice between the two. A telephone uses 8,000 samples a second, enough for speech; music uses 44,100 or more.

Task: find the note

Sample a note of 659 Hz for half a second at a sample rate of 8,000 Hz. Print samples: <n> and 8-bit size: <n> bits, worked out from the rate and duration. Then count the upward crossings, print note: <freq> Hz, and play that frequency on the buzzer for half a second.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

import math

rate = 8000
seconds = 0.5

Challenges

  1. Sample the same note at 1,000 samples a second. Does the crossing count still find it? Why not?
  2. Mix two notes by adding their samples. What does the crossing method report now?
  3. How many minutes of 8 kHz, 8-bit sound fit in 1 MB?