Tune a radio between two stations and you hear a hiss: noise, a jumble of every pitch at once with no note in it. Pluck a string on a sitar, a guitar or a sarangi and you hear a clear note that rings and slowly fades. They sound like opposites. In this topic you will turn the first into the second with a list of random numbers and one line of arithmetic.
The drawing below is a list of 80 random numbers, noise. The computer reads them out to the speaker, 16,000 a second. When it reaches the end of the list it goes back to the start, and as it reads each number it swaps it for the average of that number and the next one.
Before you pluck it for the first time: the list starts as pure noise. What do you think you will hear?
Pick an answer to see why.
You need lists and dictionaries in Python (Grade 9) and how a computer stores sound as samples (Grade 7). Nothing about music is needed: what a note and a raag are is explained here.
Try it
Pluck the string a few times, then try these.
- Averaging: None. Now each number is read out again unchanged every time round. You still hear a note, a harsh, buzzy one, and it never fades. Noise repeated over and over is a note. Its pitch is how many times a second the list comes round: 16,000 ÷ 80 = 200 times a second.
- The list’s length. Make the list shorter and the note goes higher; make it longer and it goes lower. Watch the Pitch readout.
- A little averaging (each number mostly itself, with a little of the next). The note stays bright for longer: its quick wiggles fade slowly. Half (each number swapped for the plain average of it and the next) mellows fastest, and its note fades sooner.
- Look at the list after 0 passes, then a few, then many. The jagged noise smooths out into a gentle wave, and the wave shrinks.
- Sitar’s bridge. A buzz that keeps coming back while the note rings. You will build this in Step 5, and see how far it is from a real sitar.
How it works
Two things happen together: going round the list makes a note, and the averaging shapes it into a string’s.
A pattern that repeats is a note
Your ear doesn’t hear numbers; it hears the air pushed back and forth. Any shape of wave at all, smooth or jagged, that repeats exactly 200 times a second sounds like a note with a pitch of 200 times a second. Scientists write “times a second” as hertz (Hz): 200 Hz.
So the length of the list sets the pitch: a list of n numbers, read at 16,000 a second, comes round 16,000 ÷ n times a second. That is why the noise, repeated, already sounds like a note. What it sounds like, its tone, comes from the shape that repeats.
Averaging smooths away the fast wiggles
Noise is a mixture of every kind of wiggle: slow ups and downs across the whole list, and quick zigzags from one number to the next. The averaging treats them very differently.
- In a slow up and down, each number is nearly the same as the next. Their average is nearly the number itself: it hardly changes.
- In a zigzag, up, down, up, down, each number is the opposite of the next. Their average is about zero: it almost vanishes in one pass.
This program puts smooth waves into a list of 80 numbers, with 1, 2, 4 and up to 40 ups and downs along it, and averages each one 100 times round, the way the string does:
import math n = 80for k in [1, 2, 4, 8, 20, 40]: # a smooth wave that goes up and down k times along the list wave = [math.sin(2 * math.pi * k * i / n) for i in range(n)] for p in range(100): # 100 times round the list wave = [(wave[i] + wave[(i + 1) % n]) / 2 for i in range(n)] print(f'{k:2} ups and downs: {max(wave):.3f} of it left')Output
1 ups and downs: 0.926 of it left 2 ups and downs: 0.734 of it left 4 ups and downs: 0.290 of it left 8 ups and downs: 0.006 of it left20 ups and downs: 0.000 of it left40 ups and downs: 0.000 of it leftA wave with 2 ups and downs along the list goes up and down twice as often as the list comes round: it is a higher sound, an overtone of the note. Overtones with many ups and downs vanish quickly; the note itself, the one up and down, lasts. Anything that lets slow waves through and stops fast ones is called a low-pass filter, and averaging two neighbors is the simplest one there is.
That is what a real string does too. A plucked string starts with a sharp kink, full of high overtones that sound bright and twangy. They lose their energy fastest, so the sound turns mellow, and then the note itself dies away. The program never models a string, its tension or its weight; it only does the one thing that matters to the ear.
Program it
In Python, with your program’s sound played by the page. from sound import play gives you play(samples, rate): a list of numbers from −1 to 1, rate of them a second. (sound is this page’s own module; on your own computer you would need a sound library instead.) Press Run, then Play under the output. Nothing plays until you press it.
Step 1: a string in a list
The list ring is the string. here goes round and round it, using % to go back to 0 after the end. Each number is sent to the sound before it is swapped for the average of itself and the next.
import randomfrom sound import play RATE = 16000 # samples a second def pluck(n, seconds): noise = [random.uniform(-0.5, 0.5) for _ in range(n)] # n numbers of noise middle = sum(noise) / n ring = [v - middle for v in noise] # balanced round zero, like a string at rest samples = [] for i in range(int(RATE * seconds)): here = i % n # where we are in the list after = (i + 1) % n # the next place (after the end, back to 0) samples.append(ring[here]) ring[here] = (ring[here] + ring[after]) / 2 return samples note = pluck(80, 2)print(len(note), 'samples')play(note, RATE)Output
32000 samplesLook at the start of the wave under the output: jagged noise. Move the close-up along: by half a second it is a smooth wave that repeats. Change 80 to 40, Run, and Play: about an octave higher.
Why take middle away? The average of the noise isn’t exactly zero, and a string at rest is in the middle. A wave pushed to one side would waste the speaker’s movement.
Step 2: the sound remembers the string
There is a neater way to write the same thing. The number about to be read out was put into the ring exactly n samples ago, as the average of the two numbers read out n and n − 1 samples before. So the sound can be built from its own past, with no ring at all:
import randomfrom sound import play RATE = 16000 def pluck_round(n, seconds): noise = [random.uniform(-0.5, 0.5) for _ in range(n)] middle = sum(noise) / n ring = [v - middle for v in noise] samples = [] for i in range(int(RATE * seconds)): here = i % n after = (i + 1) % n samples.append(ring[here]) ring[here] = (ring[here] + ring[after]) / 2 return samples def pluck(n, seconds): noise = [random.uniform(-0.5, 0.5) for _ in range(n)] middle = sum(noise) / n samples = [v - middle for v in noise] for i in range(n, int(RATE * seconds)): samples.append((samples[i - n] + samples[i - n + 1]) / 2) return samples random.seed(7)a = pluck_round(80, 2)random.seed(7)b = pluck(80, 2)print('The same sound?', a == b)Output
The same sound? Truerandom.seed(7) makes both functions get the same random numbers, so their sounds can be compared. Every one of the 32,000 samples is the same. The new pluck is the one Karplus and Strong wrote down: each sample is the average of the two samples n and n − 1 places back. The steps after this use it.
Step 3: measure the pitch
Is the pitch 16,000 ÷ n, or 16,000 ÷ (n − ½)? Count it. Each time the wave rises through its middle, it has been round once. Count those rises in one second of the note, after it has settled:
import random RATE = 16000 def pluck(n, seconds): noise = [random.uniform(-0.5, 0.5) for _ in range(n)] middle = sum(noise) / n samples = [v - middle for v in noise] for i in range(n, int(RATE * seconds)): samples.append((samples[i - n] + samples[i - n + 1]) / 2) return samples def count_rises(samples, start, end): """How many times the wave rises through its middle between two times (in seconds).""" part = samples[int(start * RATE):int(end * RATE)] middle = sum(part) / len(part) count = 0 for i in range(1, len(part)): if part[i - 1] < middle <= part[i]: count = count + 1 return count random.seed(1)n = 40note = pluck(n, 1.5)print(f'Rate ÷ length: {RATE / n:.1f} Hz')print(f'Rate ÷ (length − ½): {RATE / (n - 0.5):.1f} Hz')print(f'Counted in 1 second: {count_rises(note, 0.5, 1.5)}')Output
Rate ÷ length: 400.0 HzRate ÷ (length − ½): 405.1 HzCounted in 1 second: 405405, not 400: the half matters. A musician can hear the difference: 405 is about a fifth of the way from 400 to the next note of a piano.
The program counts from half a second to a second and a half, because at the start the noise still has many quick wiggles that cross the middle several times in one round.
Step 4: a raag, plucked
A raag is the set of notes, and the rules for moving among them, that a piece of Indian and Nepali classical music keeps to. Bhupali (Bhoopali), usually played early in the night, uses only five notes: Sa Re Ga Pa Dha. Each is a fixed fraction of Sa’s pitch; with the whole-number fractions many musicians tune to, Re is 9⁄8 of Sa, Ga 5⁄4, Pa 3⁄2 and Dha 5⁄3, and the next Sa up (Sa′) is twice Sa.
To play a note of pitch p, the list needs about 16,000 ÷ p + ½ numbers. A dictionary keeps each note’s length by its name:
import randomfrom sound import play RATE = 16000 def pluck(n, seconds, damp=0.996): noise = [random.uniform(-0.5, 0.5) for _ in range(n)] middle = sum(noise) / n samples = [v - middle for v in noise] for i in range(n, int(RATE * seconds)): samples.append(damp * (samples[i - n] + samples[i - n + 1]) / 2) return samples # Raag Bhupali's notes, as fractions of Sa's pitchBHUPALI = [('Sa', 1), ('Re', 9 / 8), ('Ga', 5 / 4), ('Pa', 3 / 2), ('Dha', 5 / 3), ("Sa'", 2)]SA = 100 # Sa's list lengthsa_pitch = RATE / (SA - 0.5) lengths = {}for name, ratio in BHUPALI: pitch = sa_pitch * ratio # the pitch this note should have n = round(RATE / pitch + 0.5) # the nearest list length lengths[name] = n print(f'{name:4} should be {pitch:5.1f} Hz: a list of {n} gives {RATE / (n - 0.5):5.1f} Hz') up = ['Sa', 'Re', 'Ga', 'Pa', 'Dha', "Sa'"]tune = []for name in up + up[::-1]: tune = tune + pluck(lengths[name], 0.38)print(len(tune) / RATE, 'seconds')play(tune, RATE)Output
Sa should be 160.8 Hz: a list of 100 gives 160.8 HzRe should be 180.9 Hz: a list of 89 gives 180.8 HzGa should be 201.0 Hz: a list of 80 gives 201.3 HzPa should be 241.2 Hz: a list of 67 gives 240.6 HzDha should be 268.0 Hz: a list of 60 gives 268.9 HzSa' should be 321.6 Hz: a list of 50 gives 323.2 Hz4.56 secondsPlay it: the five notes up to Sa′ and back down, the way a student sings a raag’s scale. A list’s length must be a whole number, so some notes come out a little off: Sa′ is 323.2 Hz instead of 321.6, about a tenth of the way to the next note of a piano. Challenge ★★★ finds a Sa whose list gets every note closer.
damp is new. Each time round, the average is made 0.4% smaller: a real string also loses energy to the air and the instrument’s body, and without it the low notes ring on much longer than a string’s would.
Step 5: a sitar-like note
Pluck a sitar and it doesn’t sound like a guitar. Two things make the difference.
- The bridge. On a guitar, each string leaves the bridge from a sharp edge. A sitar’s main bridge is wide, with a gently curved top that the string lies along and lifts off as it swings. Shaping that curve is called jawari, and it is skilled work. As the string vibrates, the point where it leaves the bridge moves, so the string keeps touching the bridge and lifting off it in every vibration. That is the sitar’s buzzing, bright, singing tone, and it stays bright while the note rings.
- The sympathetic strings (taraf). Under the frets run about a dozen extra strings (13 on many sitars) that nobody plucks. The player retunes them for each raag, to its notes. When a played note matches one of them, that string starts to ring by itself, softly, and keeps ringing: a halo round the note.
A program can imitate both, simply. For the bridge: whenever the wave is below the middle line (the string down on the bridge), give it a small random rattle, in proportion to how far down it is. For the taraf: five more strings, still to start with, one for each note of Bhupali, each shaken a tiny amount by the main string’s sound.
import randomfrom sound import play RATE = 16000 def pluck(n, seconds, buzz=0.0, damp=0.996): noise = [random.uniform(-0.5, 0.5) for _ in range(n)] middle = sum(noise) / n samples = [v - middle for v in noise] for i in range(n, int(RATE * seconds)): new = damp * (samples[i - n] + samples[i - n + 1]) / 2 if samples[i - n] < 0: # down on the bridge: it rattles new = new + buzz * samples[i - n] * random.uniform(-1, 1) samples.append(new) return samples def sympathy(main, lengths, pull=0.002, damp=0.999): """Strings that nobody plucks, shaken a little by the main string's sound.""" strings = [] for n in lengths: s = [0.0] * n # still, to start with for i in range(len(main)): s.append(damp * (s[i] + s[i + 1]) / 2 + pull * main[i]) strings.append(s[n:]) return strings def loudness(samples): end = samples[-RATE // 2:] # the last half second return (sum(v * v for v in end) / len(end)) ** 0.5 random.seed(3)TARAF = {'Sa': 100, 'Re': 89, 'Ga': 80, 'Pa': 67, 'Dha': 60}print('plucked', ''.join(f'{name:>5}' for name in TARAF))for played, n in [('Sa', 100), ('Pa', 67), ('Ma', 75)]: main = pluck(n, 1.5) rings = sympathy(main, TARAF.values()) print(f'{played:7}', ''.join(f'{1000 * loudness(s):5.1f}' for s in rings)) plain = pluck(100, 2)main = pluck(100, 2, buzz=0.4)rings = sympathy(main, TARAF.values())sitar = [0.6 * (m + sum(r[i] for r in rings)) for i, m in enumerate(main)]print('loudest sample:', round(max(abs(v) for v in sitar), 2))play(plain, RATE)play(sitar, RATE)Output
plucked Sa Re Ga Pa DhaSa 13.6 0.4 0.5 0.9 0.6Pa 1.4 0.6 0.6 12.2 0.8Ma 0.7 0.7 0.7 0.9 0.9loudest sample: 0.32Play the two sounds one after the other: a plain plucked string, then the same Sa with the bridge’s buzz and the taraf.
Each row is one pluck, and each number how loudly a sympathetic string is ringing in the last half second of it (in thousandths). The numbers show the taraf working. Pluck Sa, and the Sa string rings 15 to 30 times as loud as the others; pluck Pa, and the Pa string answers. Pluck Ma, a note that isn’t in Bhupali, and none of them answers much. Nobody told the program which string should ring: a string driven at its own pitch builds up, a little more each time round, the way a swing goes higher when you push it at the right moment. That is resonance.
The sounds are added together, and the sum could go past 1, so the program makes it quieter (0.6 ×) and checks the loudest sample.
What your program shows
- Noise becomes a note as soon as it repeats, at 16,000 ÷ (n − ½) times a second: you counted 405 for a list of 40.
- Averaging is a low-pass filter: the quick wiggles die in a few rounds and the slow ones last, so the sound mellows and fades like a string’s.
- Whole-number lengths can’t put every note of a raag exactly in tune.
- Sympathetic strings resonate by themselves: the taraf string tuned to the plucked note rang 13.6 against 0.4 to 0.9 for the rest.
That is why the trick mattered. One addition and one halving a sample, and a cheap chip of the 1970s played notes that sounded like a guitar. And it was one of the first steps towards a new way of making sound: from a model of the instrument itself (see “Modeling the bridge”, below).
Going further still
A drum from the same loop
Karplus and Strong also found a drum. Toss a coin for each new sample, and on heads turn it upside down:
What it can’t do
The program buzzes and resonates, but no one would mistake it for a sitar. A real sitar has much more:
- A body. The string’s sound comes out through a hollow gourd and a wooden top, which make some pitches louder than others. The program sends the string straight to the speaker.
- The real bridge. The jawari’s buzz comes from the string wrapping onto the curve and lifting off it, and it changes as the swing dies away. In 1982 the mathematicians Burridge, Kappraff and Morshedi worked out the motion of a string against a curved obstacle like this; it is much harder than averaging two numbers. The program’s random rattle only gives a buzz that comes once in each vibration.
- The player. The sitar is played with a wire pick (mizraab) on the finger, and its notes slide from one to the next (meend) as the player pulls the string sideways across a fret. Every pluck of the program is the same kind of pluck.
- Exact tuning. Lengths are whole numbers. Jaffe and Smith’s fix was a filter that delays the sound by a fraction of a sample.
Challenges
Three challenges, each checked on the page: write your function in the program above the checker (press Edit), then press Check my program. The examples show what your function should give; there are more tests you can’t see.
★ How long a list?
Write list_length(pitch): the length of list that plays nearest to pitch (in Hz) at 16,000 samples a second. Use the exact rule from Step 3: a list of n plays at 16,000 ÷ (n − ½), so n is about 16,000 ÷ pitch + ½. Round to the nearest whole number.
RATE = 16000 def list_length(pitch): # Give back the whole-number list length that plays nearest to pitch. return 0 print(list_length(160.8))★★ How long does it ring?
Write ring_time(samples, rate): how many seconds the sound rings. Find the loudest sample’s size (ignoring its sign), then the last sample whose size is at least a thousandth of that. Give back its time: its place in the list divided by rate (the first sample is at time 0). If every sample is 0, give back 0.
def ring_time(samples, rate): # Give back the time of the last sample at least 1/1000 as big as the loudest. return 0 print(ring_time([0.5, -0.4, 0.2, -0.01, 0.0004, 0.0001], 10))★★★ The best Sa
In Step 4, a Sa list of 100 put Sa′ about a tenth of a note out of tune. Some lengths do better. Write best_sa(ratios, low, high). Each note’s fraction of Sa comes as a pair [top, bottom]: [9, 8] for Re’s 9⁄8. Try every Sa length from low to high; for each, find every note’s list length (as in Step 4) and how far out of tune it is in cents, abs(1200 * math.log2(got / wanted)) (100 cents is one step of a piano). A Sa length is as good as its worst note. Give back the Sa length whose worst note is closest to in tune; if two are equally good, the shorter.
import math RATE = 16000 def best_sa(ratios, low, high): # Try each Sa length from low to high; give back the one whose worst note is best. return low print(best_sa([[1, 1], [9, 8], [5, 4], [3, 2], [5, 3], [2, 1]], 90, 110))Questions
A list of 50 numbers is read at 16,000 samples a second with averaging. About what pitch do you hear?
Pick an answer to see why.
Why does the note get mellower as it rings?
Pick an answer to see why.
Explain why a list of random numbers, read round and round with no averaging, sounds like a note and not like a hiss.
Answer
A friend’s pluck uses (samples[i - n] + samples[i - n + 1]) / 2.1 and their note dies much faster than yours. Why? What else could they have changed to make it ring longer instead?
Answer
In Step 5, the Sa sympathetic string rang loudly when Sa was plucked and hardly at all when Ma was. Explain why, using the word resonance.
Answer
Give two reasons the program’s sitar-like note is not a real sitar, and one way a real sitar player changes the sound that the program can’t.
Answer
Where to go next
- Grade 7, Sound as samples: samples a second and bits a sample, the numbers every sound program works with.
- Grade 8, Recording clean sound: noise as the thing you don’t want, and how to keep it out of a recording.
- Grade 9, Lists, Strings and Dictionaries: the lists and dictionaries this topic’s programs are built from.
- How Can a Computer Never Lose at Tic-Tac-Toe?: another program that does something surprising with a few lines and a list.
