How Counting in Binary Numbers can Cause Auditory Illusions
Let’s generate Shepard Scales from a PIC microcontroller in Assembly Language
In my previous article, I talked about the simplicity of Assembly Language and about the world’s smallest hobbyist computer — a PIC microcontroller. In this article, I want to pick up where we left off and introduce you to a fascinating illusion called Shepard Tones — an illusion of ever-ascending/descending scales — and how you can create that complex illusion with a simple PIC that can only emit ON-OFF signals.
The fundamentals of Shepard Tones
There is a family of auditory illusions called Shepard Tone. Hearing is believing. Here is a great example on YouTube:
In the first half of the video, the pitch seems to go down and down; in the second half, it goes up and up, with no apparent end. In fact, the sound is on repeat so it can go on forever! How can this be?
The trick, as described in Roger Shepard’s paper, is to play multiple notes at once at octave intervals. Because of the periodicity in how we perceive pitch, if we hear a note that superposes 10 notes of the key C, D, or E at octave intervals, we will have difficulty in separating them out. Furthermore, these notes are passed through a bell-shaped envelope filter that boosts the middle frequency and suppresses the high and low frequencies. After a full cycle, the top-most note fades away to be replaced by the bottom-most note, resulting in a never-ending loop of ascending / descending notes.
Okay, so the principle is fairly straightforward. How then, shall we create this superposition of sound waves at octave intervals? This is where our PIC microcontroller plays its trick.
We said that a PIC has byte-long Input/Output ports with 8 pins each. PIC16F628A has two ports, Port A and Port B. Let’s now make each pin on Port B output a wave at octave intervals. We emit waves by switching the output pins ON and OFF. The faster you switch, the higher the wave frequency. Let’s say that pin 0 emits a wave with the highest frequency, and pin 7 emits a wave with the lowest frequency. The result looks like this:
RB0-RB7 corresponds to the outputs of pins 0–7. Looking at each individual pin from left to right, you can see that they are switched ON and OFF at different frequencies with a scaling factor of two. Now, let’s look at each port output value as a whole. Do you see something interesting?
Yes, it’s binary counting! (The title gave it away, didn’t it?) The left most output amounts to 00000000, then 00000001, 00000010, 00000011, … and so on. How simple and yet beautiful! By just counting up binary numbers at 2⁴ * 440Hz, a PIC is able to produce the basic wave components for a Shepard Tone of key A. By counting faster or slower, the PIC can alter the pitch. While recordings of a Shepard Tone may require MBs of memory, the Assembly code captures the fundamentals of Shepard Tones in a simple loop!
I will now give a brief overview of the code. The full assembly code is available at this GitHub repository. In case you aren’t interested in the implementation, scroll to the final section where you can hear for yourself some Shepard Tone samples generated by the circuit!
Code for the Shepard Tone Generator
We start by implementing the core functionality of producing a Shepard Tone. The relevant code is in tone_generator.asm
. Varying the base frequency for different keys, i.e. how fast it counts, is a little involved. For now, we can abstract that complexity away by calling a subroutine DLYSET
which will provide us with an appropriate amount of delay. CNT0
is the counter that counts down from 0xFF, 0xFE, … down to 0x00 (this weird notation is called HEX, and it represents numbers with a base 16 where every digit is from 0–9 to A-F), using a DECFSZ
command. You could easily substitute this with an INCFSZ
command and count up if you’d prefer. CNT1
counts down the number of times this loop has to be repeated in order to make the note last for a quarter of a second. A PIC command consumes 1 μS or 2 μS, depending on whether it involves a jump. The amount of μS required for each command is shown in the comment. TONE_TM
is there just to make it consume an extra 2 μS to make every loop consume the same amount of time.
So that was just 14 lines of code for the heart of the Shepard Tone! How beautiful!
Well, not so easy. We now have to figure out the exact amount of delay in DLYSET
and the exact amount of loops CNT1
to generate a note with the key of our desire. This is handled in sections DLYSET
and NOTESET
.
Here are two variables, TONE
and NOTE
. The reason for this will become clearer when you see the entire code. Basically, because Shepard Scales repeat the same sequence of notes but with a different offset, it makes sense to just define the sequence of notes defined relative to the offset TONE
, and evaluate the absolute NOTE
on the fly. The absolute NOTE
should wrap around if an octave is reached, so it always has a value between 0x00 and 0x0B (=11).
For each NOTE
, we want to retrieve the relevant wait subroutine and the loop count CNT1
. This is happening whenSETDATA
is called. Here, I have used a trick called lookup tables. Essentially, this is an array of commands in order in memory so that they could be jumped to conditionally based on the value stored in the working memory. It is typical to have a GOTO
or a RETLW
(retrieve a hard-coded value, store it in working memory, and return) statement to make the PIC have varying behaviour based on the value of the working memory.
The rest of the code mainly consists of two parts. First is about creating the right amount of delay necessary for a sound wave to be produced. For instance, if you want a note with a key A, that has a frequency of 2⁴ * 440Hz, you will have to turn the least significant bit ON and OFF 2 * 2⁴ * 440 times in a second, which gives you 71 μS for each loop. There is an overhead of 18 μS in the TONE_LP
loop, so the delay in TONEDLY_A
should amount to 53 μS.
The second part of the code is to do with switching sound tracks. There are five songs I’ve hard-coded in the built-in memory, and with external input, you can switch between them. The external input is read in by Port A.
Shepard Tone Generator Circuit + Extra Code
A fun but also an labour intensive part is building the circuit. In addition to the core PIC that generates the Shepard Tone, I added two additional PICs, one for track selection, and the other for displaying the tracks on an LED display. The corresponding assembly code are track_selector.asm
and track_display.asm
respectively. Then these PICs are wired together as shown in this diagram.
The three PICs in the diagram from top to bottom correspond to the track selector, tone generator, and track display PIC respectively. The track selector PIC is connected to four buttons: PLAY, STOP, PREV, NEXT. You can select the sound track using PREV and NEXT, and once you decide on a track, you can press PLAY. Then the selected track will be outputted from Port B of the PIC and is transmitted to the other two PICs, which receive that signal at their Port A. The tone generator emits 8 wave signals from its 8 pins, which are then combined to make a single output signal.
There is an additional post-processing step. Do you remember the bell-shaped envelope at the beginning of this article? You would want to apply a filter to the generated wave signal so that the middle-range is boosted while the higher and lower frequencies are cut off. This can be achieved either by a passive mid-range filter circuit made of resistors and capacitors, or with an active filter that uses an op-amp. I went for the former approach.
And finally, after a couple of months’ work, I had a Shepard Tone generator, ready to be exhibited at our school festival!
Generated samples
In case you don’t want to go through the daunting work of shopping for components, soldering, programming, and compiling, below are some example recordings, all taken from the Shepard Tone Generator that I have created. Apart from cropping and concatenating, no post-processing has been performed on the recorded signals.
Here is the first example. This is already two cycles, but you can put this on repeat to fully appreciate how never-ending this scale can be!
If you listen carefully, you might pick up the base note which drops at a certain point. Now, let’s compare this against a non-Shepard Tone scale with the same keys.
Now the difference is evident! The first scale might be able to catch the listener unawares whereas the second one is plainly disillusioning!
Here is another example that I like, partly because I got the idea from those boring scales you have to practice when you are learning to play the piano:P
Finally, I tried to be more experimental by making some very long pieces of music just out of Shepard Tones. Most of it was rather a challenge, since any piece has to fit within a range of an octave to cause a convincing illusion (you can work around with this by making the cut-off frequencies of the mid-range filter depend on which part of the song it is playing, but that can get pretty advanced). I will share with you an attempt at doing this with Greensleeves, which I think went fairly well despite its range of pitch.
This whole recording is 10 min long, and it consumes 98MB as a WAV file. Remember that a PIC has only 4KB of memory? Despite that, it was able to withhold this entire song, along with four other songs in its built-in memory. I think it is astounding and beautiful that a simple machine code of a few hundred bytes can express this rich information in the most compressed and fundamental form!
Thank you for reading till the end!
[1] Roger Shepard (1964) “Circularity in Judgements of Relative Pitch”, Journal of the Acoustical Society of America 36 (12): 2346–2353.
Related articles: