How Counting in Binary Numbers can Cause Auditory Illusions
Tags: Coding, Sound, Computer Science, Programming, Microcontrollers

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?

Image from the original paper by Roger Shepard¹

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:

Diagram showing how each pin on port B should be switched to produce wave signals at octave intervals. RB0-RB7 are bits on Register B, which are exported to Port B that maps onto the output pins.

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 when SETDATA 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

Circuit diagram for the Shepard Tone Generator

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!

Completed Shepard Tone Generator

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!

Example of a Shepard Tone generated by the PIC circuit

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.

Example of the same scale but without the superposition of waves at octave intervals

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

Example of another Shepard Tone generated by the PIC circuit

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: