Sound

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

F8.8Data representationGCSE15 min

Covers OCR J277 1.2.4 Data storage · AQA 8525 3.3.7 Representing sound · Edexcel 1CP2 2.2.3 Sound · Edexcel 1CP2 2.2.4 Limits of binary representation

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.

A 440 Hz wave sampled 4,000 times a second: a measurement every 0.25 ms, 20 of them in 5 ms012345ms0.25 ms between samplesgrey line: the sound wavegreen dots: the samples stored
The computer keeps only the green dots, one every 0.25 ms. Between them it knows nothing, so the more samples a second, the more closely the dots follow the wave.

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.

The 440 Hz wave rounded to 2-bit samples, 4 levels, which make a rough staircase, and to 8-bit samples, 256 levels, which follow the wave closely2-bit: 4 levels0 = 001 = 012 = 103 = 118-bit: 256 levels025501234ms
The same 16 moments measured both times. With 2 bits each measurement is rounded to the nearest of only four levels, and the short red lines show how far it moves; with 8 bits the rounding is too small to see.

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

The 523 Hz note sampled 8,000 times a second, with each place the samples cross the middle going up marked: one mark per cycle of the wavethe middle12344 rises in the first 8 ms.In the whole 0.5 s: 261 rises, and 261 ÷ 0.5 = 522, so about 522 Hz
Each green arrow is a place where one sample is below the middle and the next is on it or above. The wave rises through the middle once every cycle, so counting the rises in a second gives the frequency.

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?

Past paper questions on this

Real exam questions that test this, each worked through with a program you can run.