Sound and MIDI

Sample rate, sample resolution, the Nyquist theorem and aliasing, and MIDI events played on the robot's piezo.

A7.8Data representationA level30 min

Covers AQA 7517 4.5.6.7 Digital representation of sound · AQA 7517 4.5.6.8 Musical Instrument Digital Interface (MIDI) · Eduqas A500QS 2.3 Data representation and data types

Do this lesson in the simulator

Sound is an analogue pressure wave. At GCSE you sampled it, worked out file sizes and found a note from its samples. A level adds the rule that decides how fast you must sample, what goes wrong if you do not, and a completely different way to store music: not the sound itself, but instructions for making it.

Sampled sound

To record sound, a microphone turns the pressure wave into a voltage and an ADC samples it (lesson A7.7). To play it back, a DAC turns the samples back into a voltage for a loudspeaker. Two settings decide the quality:

  • Sampling rate: the number of samples taken per second, in hertz (Hz). CD audio uses 44,100 Hz.
  • Sample resolution: the number of bits used for each sample. CD audio uses 16 bits, so each sample is one of 65,536 levels.

A higher sampling rate captures faster changes, so higher frequencies. A higher resolution rounds each sample less, so the recording is closer to the original, with less quantisation noise. Both make the file bigger:

sound file size in bits = sampling rate × sample resolution × length in seconds

and multiply by the number of channels for stereo. One minute of CD stereo audio is 44,100 × 16 × 2 × 60 = 84,672,000 bits, which is 10,584,000 bytes, about 10.6 MB.

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

cd = sound_bits(44100, 16, 60, channels=2)
print(cd, "bits =", cd / 8 / 1000 ** 2, "MB")
phone = sound_bits(8000, 8, 60)
print(phone, "bits =", phone / 8 / 1000, "kB")

Run this in the simulator

The Nyquist theorem

How often must you sample? The Nyquist theorem says:

to record a signal accurately, the sampling rate must be at least twice the highest frequency in the signal.

People hear up to about 20,000 Hz, so audio sampled at 44,100 Hz keeps every frequency we can hear. Telephone speech is sampled at 8,000 Hz, so nothing above 4,000 Hz survives; that is why voices sound thin on a phone.

People hear up to 20,000 Hz. Sampling at 44,100 Hz keeps everything up to 22,050 Hz; sampling at 8,000 Hz keeps only up to 4,000 Hz04,0008,00012,00016,00020,00024,000frequency (Hz)what people hearCD audiosampled at 44,100 Hzkept up to 22,050 Hztelephonesampled at 8,000 Hzkept up to 4,000 Hzheard, but not recorded
Each bar is half the sampling rate, the highest frequency the Nyquist theorem lets it record. CD audio covers all of hearing with a little to spare; a telephone throws away everything above 4,000 Hz.

Sample below the Nyquist rate and the frequencies that are too high are not just lost, they come back wrong: the samples fit a lower-frequency wave just as well. This is called aliasing. The robot's piezo can play A at 440 Hz. Sampled 8,000 times a second, the samples still show 440 Hz. Sampled only 600 times a second, below the 880 Hz the theorem demands, the samples describe a wave at 160 Hz.

440 Hz sampled 8,000 times a second: the dots follow the wave. Sampled 600 times a second: the dots fit a 160 Hz wave insteadsampled at 8,000 Hz: about 18 dots per wavesampled at 600 Hz, below the 880 Hz it needsthe dots join up into 160 Hz02.557.51012.5msgrey: the 440 Hz note
Both rows show the same 440 Hz note. At 8,000 Hz the samples trace it closely. At 600 Hz the wave goes most of the way through a cycle between samples, and the few dots it leaves lie exactly on a slower 160 Hz wave, which is what gets played back.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
import math

def apparent_frequency(freq, rate, seconds=1.0):
    """Sample a sine wave and count its upward zero crossings per second."""
    samples = [math.sin(2 * math.pi * freq * i / rate) for i in range(int(rate * seconds) + 1)]
    rises = 0
    for i in range(1, len(samples)):
        if samples[i - 1] < 0 <= samples[i]:
            rises = rises + 1
    return round(rises / seconds)

for rate in [8000, 1000, 600]:
    heard = apparent_frequency(440, rate)
    print("sampled at", rate, "Hz, the recording plays", heard, "Hz")
    tone(heard, 0.4)

Run this in the simulator

Listen to the last note: that is what a recorder sampling at 600 Hz would play back. Real ADCs put a low-pass filter in front of the sampler to remove frequencies above half the sampling rate, so they cannot alias.

MIDI

MIDI, Musical Instrument Digital Interface, does not store sound at all. It stores event messages, instructions to an instrument or synthesiser about what to play. The main messages are:

  • note on and note off, each with a note number (0 to 127, where 60 is middle C and 69 is the A at 440 Hz) and a channel, so several instruments can be controlled at once
  • velocity (0 to 127), how hard the note is struck, which usually controls loudness
  • messages that change the instrument (a program change) or controls such as sustain

Durations come from the time between a note on and its note off. A MIDI file is a list of these events with their timings.

The four MIDI events on a time line: each note is a note on message at its start and a note off message at its end60646772note numbernote on 60note off 6000.250.50.7511.25time (s)4 notes, 8 messages
A MIDI file holds only the messages: which note starts, which stops, and when. The note's length is the time between its note on and note off; the sound itself comes from whatever instrument plays it.

Each note number is one semitone. Twelve semitones make an octave, and an octave doubles the frequency, so:

frequency = 440 × 2^((note - 69) / 12)

Note number Note Frequency (Hz, rounded)
57 A below middle C 220
60 middle C 262
69 A 440
72 C above middle C 523

Advantages of MIDI over sampled sound. Files are far smaller, because a few bytes describe a note that would take thousands of samples. The music is easy to edit: change a note, the tempo or the instrument without re-recording. The same file can be played on any instrument sound.

Disadvantages. The sound depends on the synthesiser playing it, so it can sound different on different devices. It can only describe music as notes: speech, singing and real recordings cannot be stored as MIDI.

Task: play the MIDI events

events is a list of (note, seconds) pairs: note is a MIDI note number (0 to 127) and seconds is how long to play it.

  1. Write frequency(note), which returns 440 × 2^((note - 69) / 12) rounded to the nearest whole number with round, as an int.
  2. For each event in order, print note <note> = <frequency> Hz and play it on the piezo with tone(frequency, seconds).
  3. After the last note, print minimum sample rate: <n> Hz, where n is twice the highest frequency you played: the Nyquist rate for recording those notes' fundamental frequencies.

Calculate every frequency; do not type them in.

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

events = [(60, 0.25), (64, 0.25), (67, 0.25), (72, 0.5)]

def frequency(note):
    return 440

Challenges

  1. Add a velocity to each event and print it next to the note. Why can the piezo not show it?
  2. How many bytes would the four notes take as 8-bit mono samples at 8,000 Hz? Compare that with a few bytes per MIDI event.
  3. At what sampling rate would note 108, the top of a piano, alias?