Pseudo-Polyphony With Micro:bit

by microbit-noob in Circuits > Speakers

109 Views, 3 Favorites, 0 Comments

Pseudo-Polyphony With Micro:bit

IMG_2771.jpeg

The Micro:bit can only play one note at a time (with minor exceptions). However, with a clever trick that retro video games used back in the day, we can create the illusion of multiple notes played simultaneously. This allows chords and multiple voicelines to be played!

This effect works by rapidly alternating between played notes, making it seem as though they're playing at the same time. If you are familiar with music theory, it is equivalent to a very fast arpeggio.

In this project, I will replicate this technique on the Micro:bit.


Note: I will be writing code in JavaScript/TypeScript using the Microsoft MakeCode editor. A little coding experience is helpful for this project; however, you're welcome to copy and paste my code, which I will post in full at the end of this Instructable.

Supplies

IMG_2809.png
IMG_2772.JPG

(Click the image above to see annotations!)


All of these materials are included in the KS4007/KS4008 Keyestudio Starter Kit (except for the Micro:bit on the KS4007).

You will need:

  1. 1x Micro:bit v2.0
  2. 1x USB Cable (Micro to Type-A)
  3. 1x Breadboard
  4. 1x Micro:bit T-Type Shield
  5. 1x IR Receiver
  6. 1x Remote Control
  7. 4x Button Modules
  8. approx. 15 or more male-to-male breadboard wires

Install the Makerbit IR Receiver Extension

extension_1.png
extension_2.png
extension_3.png

First, create a new project and install the ‘MakerBit’ extension, which will allow you to work with the IR receiver. The extension should show up in the side bar once installed.

Connecting the IR Reciever

IMG_2792.jpeg
IMG_2794.png
IMG_2798.png
IMG_2810.jpeg

This receiver has 3 pins:

  1. The left pin connects to a digital read pin of your choice. The IR receiver will output signals through this pin. (Recommended not to use pins 0 to 2 if you want to use the TouchPins for extra buttons.)
  2. The middle pin connects to GND (ground).
  3. The right pin connects to 3V.

Testing the IR Receiver

IMG_2801.jpeg
Screenshot 2025-11-08 154321.png
music.setVolume(255);
music.setTempo(120);

makerbit.connectIrReceiver(DigitalPin.P10, IrProtocol.Keyestudio);

makerbit.onIrButton(IrButton.Any, IrButtonAction.Pressed, function () {
// Plays a 262 Hz note for a short time.
music.play(music.tonePlayable(
262,
music.beat(BeatFraction.Whole)),
music.PlaybackMode.UntilDone)
});

This code will tell the Micro:bit to read the IR receiver's outputs from your chosen pin with connectIrReceiver, and the onIrButton will fire when its parameter conditions are met (in this case, IrButton.Any meaning any button on the remote control, and IrButtonAction.Pressed meaning when a button is pressed).

After that, pointing the remote control towards the IR receiver and pressing a button should play a note!


Troubleshooting

If nothing plays, here are some possible fixes:

  1. Make sure the wire connections are in properly.
  2. Check if you connected the output of the IR receiver to the correct digital pin (the code above sets it to DigitalPin.P10).
  3. Make sure the receiver's pins are connected to the right input/output.
  4. Use a different digital pin.
  5. Double-check the battery in the remote control.
  6. Check if the Micro:bit is properly inserted into the T-type shield; the front should face the breadboard.
  7. Re-download the code or hit the reset button on the back of the Micro:bit.

Assigning Musical Notes to Buttons

We will assign the button codes on the remote control to musical notes. Below is a C major scale.

// Put this at the very top of your code.

const buttonToNote: { [key: number]: number } = {
[IrButton.Number_1]: Note.C4,
[IrButton.Number_2]: Note.D4,
[IrButton.Number_3]: Note.E4,
[IrButton.Number_4]: Note.F4,
[IrButton.Number_5]: Note.G4,
[IrButton.Number_6]: Note.A4,
[IrButton.Number_7]: Note.B4,
[IrButton.Number_8]: Note.C5
};

In technical terms, this is a dictionary that we will use to map the input buttons to the output musical notes.

Feel free to pick whatever notes you wish the remote control buttons to play!


Example of Custom Note Assignment

Here is an example of what you could do! This is a D minor blues scale with some added notes.

const notVerySpecialNotes: { [key: number]: number } = {
[IrButton.Number_1]: Note.Bb3,
[IrButton.Number_2]: Note.B3,
[IrButton.Number_3]: Note.C4,
[IrButton.Number_4]: Note.D4,
[IrButton.Number_5]: Note.E4,
[IrButton.Number_6]: Note.F4,
[IrButton.Number_7]: Note.G4,
[IrButton.Number_8]: Note.GSharp4,
[IrButton.Number_9]: Note.A4,
[IrButton.Star]: Note.C5,
[IrButton.Number_0]: Note.D5
};

Code for Pseudo-Polyphony

Your paragraph text.png

Let's start coding in pseudo-polyphonic functionality!


The way I did this is to split a note up into many very short ones and play them in rapid succession. That way, with multiple notes, each short note would take turns in playing, making it seem like they're playing at the same time. (see image above)

// Stores list of notes that are currently playing
let active_notes: number[] = [];


Now we need to be able to add and remove notes from this list. Start with this empty function.

function noteOnOrOff(note: number, duration_ms: number) {
control.inBackground(function () {
// Code in this block runs in background
})
}

The function will need to know the note being pressed and for how long. control.inBackground allows the code inside to run without halting the entire program to wait for it to finish.


Add this inside the control.inBackground function.

// If note is not in active_notes, add it to active_notes, and remove it
// after duration_ms of time.

if (!(active_notes.indexOf(note) >= 0)) {
active_notes.push(note);
basic.pause(duration_ms);

if (active_notes.indexOf(note) >= 0) {
active_notes.removeAt(active_notes.indexOf(note));
}
}


After that, replace the code inside your current makerbit.onIrButton function with this.

let buttonPressed: number = makerbit.irButton() // Gets enum of last button pressed
if (buttonToNote[buttonPressed]) {
noteOnOrOff(buttonToNote[buttonPressed], music.beat(BeatFraction.Half))
}

Every time a button on the remote control is pressed, buttonPressed stores which button you pressed. Then, the code checks if that button corresponds to any note in buttonToNote. If it does, the noteOnOrOff function runs, playing the corresponding note. Here, I have the note last half a beat; however, you can change it to any duration you want.


Now, we need to loop over active_notes and play them.

loops.everyInterval(1, function () {
for (let note of active_notes) {
music.play(music.createSoundExpression(
current_waveshape,
note,
note,
255,
255,
pause_ms,
SoundExpressionEffect.None,
InterpolationCurve.Linear
), music.PlaybackMode.InBackground);
}
})

Extra Buttons

IMG_2805.jpeg
IMG_2812.png
IMG_2813.png

The button modules should be placed crossing the center divider of the breadboard. Connect one side of the button to a touch pin and the other end to GND (ground).

Use pins 0, 1, and 2 as the main pins. You can use pin 5 as an external button A, and pin 11 as an external button B.


Example

Here is an example of what you can do with these buttons. Notice how we call noteOnOrOff to play a note of our choice.

input.onPinPressed(TouchPin.P0, function () {
// Dividing a note by 2 results in the same note an octave lower.
noteOnOrOff(Note.C3 / 2, music.beat(BeatFraction.Half))
})


Troubleshooting

If the buttons do not function as intended, here are some possible fixes:

  1. Check the button connections; the metal pins should be in line with the wires.
  2. Try pressing down and to the side.
  3. Try a different touch pin (if possible).

Final Result

IMG_2834.jpeg

Your code should look something like this:

music.setVolume(255);
makerbit.connectIrReceiver(DigitalPin.P10, IrProtocol.Keyestudio);
music.setTempo(120)

const buttonToNote: { [key: number]: number } = {
[IrButton.Number_1]: Note.C4,
[IrButton.Number_2]: Note.D4,
[IrButton.Number_3]: Note.E4,
[IrButton.Number_4]: Note.F4,
[IrButton.Number_5]: Note.G4,
[IrButton.Number_6]: Note.A4,
[IrButton.Number_7]: Note.B4,
[IrButton.Number_8]: Note.C5
};

let active_notes: number[] = [];
let current_waveshape: WaveShape = WaveShape.Square;
let pause_ms: number = 20;

function noteOnOrOff(note: number, duration_ms: number) {
control.inBackground(function () {
// If note is not in active_notes, add it to active_notes, and remove it
// after duration_ms of time.
if (!(active_notes.indexOf(note) >= 0)) {
active_notes.push(note);
basic.pause(duration_ms);

if (active_notes.indexOf(note) >= 0) {
active_notes.removeAt(active_notes.indexOf(note));
}
}
})
}

makerbit.onIrButton(IrButton.Any, IrButtonAction.Pressed, function () {
let buttonPressed: number = makerbit.irButton()
if (buttonToNote[buttonPressed]) {
noteOnOrOff(buttonToNote[buttonPressed], music.beat(BeatFraction.Half))
}
})

loops.everyInterval(1, function () {
for (let note of active_notes) {
music.play(music.createSoundExpression(
current_waveshape,
note,
note,
255,
255,
pause_ms,
SoundExpressionEffect.None,
InterpolationCurve.Linear
), music.PlaybackMode.InBackground);
}
})

// BUTTONS

input.onPinPressed(TouchPin.P0, function () {
noteOnOrOff(Note.C3 / 2, music.beat(BeatFraction.Double) * 3)
})

input.onPinPressed(TouchPin.P1, function () {
noteOnOrOff(Note.G3 / 2, music.beat(BeatFraction.Double) * 3)
})

input.onPinPressed(TouchPin.P2, function () {

noteOnOrOff(Note.F3 / 3, music.beat(BeatFraction.Double) * 3)
})

input.onButtonPressed(Button.A, function () {

noteOnOrOff(Note.A3 / 3, music.beat(BeatFraction.Double) * 3)
})

Don't worry if it doesn't look the same as yours (assuming you hadn't already customized it). I recommend experimenting and adding components; make this project truly your own!

Bonus Simple Demo!

Here's a short and simple rendition of Toby Fox's Megalovania! (Yes, very unique pick, I know.)