Sound and MIDI
Sample rate, sample resolution, the Nyquist theorem and aliasing, and MIDI events played on the robot's piezo.
Do this lesson in the simulatorSound 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")
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.
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.
# 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)
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.
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.
- Write
frequency(note), which returns 440 × 2^((note - 69) / 12) rounded to the nearest whole number withround, as anint. - For each event in order, print
note <note> = <frequency> Hzand play it on the piezo withtone(frequency, seconds). - After the last note, print
minimum sample rate: <n> Hz, wherenis 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
- Add a velocity to each event and print it next to the note. Why can the piezo not show it?
- 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.
- At what sampling rate would note 108, the top of a piano, alias?