Closed microbit-carlos closed 1 year ago
I've spent a little while investing this one... There were a number of factors at play, which I've addressed in: https://github.com/lancaster-university/codal-microbit-v2/commit/ed14fa2c330a5c1b9a2904b922823a1112e20dfe
1) There was a small bug in the Mixer codebase, that meant (sometimes) the first sample of DMA buffer was not correctly loaded from the incoming data stream, and cause a small glitch in the audio output:
This bug was was surfacing itself quite a lot when used with the virtual pwm pin. Humans being humans, are very sensitive to this kind of thing, and we can hear it as a buzzy low frequency noise. This is now fixed in main branch.
2) The output frequency of the virtual pwm pin was not always completely perfectly renderable by the output PWM, which is running at 44.1kHz. This results in the output "chasing" the ideal requested frequency by alternating between the two closest approximations to the frequency that can be achieved - causing further buzzing/overtones. I've added a compile time CONFIG option (CONFIG_SOUND_OUTPUT_PIN_DISCRETE_OUTPUT), which discretizes the output of virtual PWM to only output frequencies that can be accurately rendered by the output stage. This is enabled by default. This does however create inaccuracies in the output frequency, which increase as the requested notes get higher. Here's a model of how the accuracy varies with requested pitch:
As can be seen in the diagram, this is a good trade-off for the lower (more commonly used) parts of the scale, which are the first five octaves. Octave 6 is just about OK, octaves 7+ are not. Generally, this provides an increase in humanly perceived quality for the common ranges, as octaves 7+ are generally note pleasant on the micro:bit speaker anyway... Users wanting higher, accurate frequencies should consider using the hardware PWM interface. This would come at the expense of disabling other interfaces tot he speaker of course (sound emojis, sample playback, multi-track audio etc).
3) The external crystal oscillator is now enabled by default (rather than the internal RC oscillator). This results in more accurate tones being produced.
4) I also experimented with outputting sine waves rather than square waves, which permit much more accurate frequency reproduction. This actually works very well in terms of accuracy and perceived sound quality, but results in significantly reduced sound volume from the micro:bit speaker (unusably so). The sound quality on external headphones is however the best of all tests. As the micro:bit speaker is the most common use case however, we haven't applied this change.
For future reference, the HEX files and source code of the tests are provided below (a variation of the test case provided above). Use button A/B to select the octave, then press A+B to play a chromatic scale in that octave.
#include "MicroBit.h"
#include "samples/Tests.h"
MicroBit uBit;
const int DEFAULT_TEMPO_BPM = 120;
const int MS_PER_BPM = (60000 / DEFAULT_TEMPO_BPM) / 4;
const int NOTE_STR_LEN = 6;
static void playTone(Pin *audioPin, int frequency, int ms) {
if (frequency) {
audioPin->setAnalogPeriodUs(1000000 / frequency);
audioPin->setAnalogValue(512);
}
uBit.sleep(ms);
audioPin->setAnalogValue(0);
}
static void playMelody(Pin *audioPin, char melody[][NOTE_STR_LEN], size_t len) {
// Starting from default values, optional octave & duration values are remembered
int octave = 4;
int durationMs = 500;
for (size_t i = 0; i < len; i++) {
const char *note = melody[i];
const char *note_char = &melody[i][0];
int distanceFromA = 0;
// First process the note, as its distance from A
switch (*note_char) {
case 'A': distanceFromA = 0; break;
case 'B': distanceFromA = 2; break;
case 'C': distanceFromA = -9; break;
case 'D': distanceFromA = -7; break;
case 'E': distanceFromA = -5; break;
case 'F': distanceFromA = -4; break;
case 'G': distanceFromA = -2; break;
default: target_panic(123); break;
}
// Then process the optional #/b modifiers and/or scale
note_char++;
while (*note_char != ':' && *note_char != '\0') {
if (*note_char == '#') {
distanceFromA++;
} else if (*note_char == 'b') {
distanceFromA--;
} else if ((*note_char >= '0') && (*note_char <= '9')) {
octave = (*note_char - '0');
} else {
target_panic(124);
}
note_char++;
}
// If an optional duration (single digit) is present, calculate the delay in ms
if (*note_char == ':') {
note_char++;
if ((*note_char < '0') || (*note_char > '9')) target_panic(125);
durationMs = 500;
}
// Calculate note frequency
float distanceFromA4 = (octave - 4) * 12 + distanceFromA;
int frequency = 440.0 * pow(2, distanceFromA4 / 12.0);
// Play the tone/rest for the calculated duration
uBit.serial.printf("%s -> f:%d hz, period:%d us, duration:%d ms\n", note, frequency, 1000000 / frequency, durationMs);
playTone(audioPin, frequency, durationMs);
}
}
char selectedOctave = 4;
int displayon = 1;
void flipDisplay(MicroBitEvent)
{
if (displayon)
uBit.display.disable();
else
uBit.display.enable();
displayon = !displayon;
}
void lowerOctave(MicroBitEvent)
{
if (selectedOctave > 0)
selectedOctave--;
uBit.display.print((int)selectedOctave);
}
void higherOctave(MicroBitEvent)
{
if (selectedOctave < 8)
selectedOctave++;
uBit.display.print((int)selectedOctave);
}
void playOctave(MicroBitEvent)
{
char noteScale[][NOTE_STR_LEN] = {
"C0:8", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
};
char *octaveChar = ¬eScale[0][1];
*octaveChar = selectedOctave + '0';
playMelody(&uBit.audio.virtualOutputPin, noteScale, 12);
}
int main() {
uBit.init();
uBit.audio.setSpeakerEnabled(true);
uBit.audio.setPin(uBit.io.P0);
uBit.audio.setPinEnabled(true);
uBit.audio.mixer.setSilenceLevel(0);
uBit.messageBus.listen(MICROBIT_ID_BUTTON_A, MICROBIT_BUTTON_EVT_CLICK, lowerOctave);
uBit.messageBus.listen(MICROBIT_ID_BUTTON_B, MICROBIT_BUTTON_EVT_CLICK, higherOctave);
uBit.messageBus.listen(MICROBIT_ID_BUTTON_AB, MICROBIT_BUTTON_EVT_CLICK, playOctave);
uBit.messageBus.listen(uBit.io.logo.id, MICROBIT_BUTTON_EVT_CLICK, flipDisplay);
uBit.display.print((int)selectedOctave);
while (true)
uBit.sleep(10000);
}
Sample HEX files:
Implemented in https://github.com/lancaster-university/codal-microbit-v2/commit/ed14fa2c330a5c1b9a2904b922823a1112e20dfe 🎉, thanks Joe!
This is the case with both, the on-board speaker, and earphones to P0 & GND.
This example program plays notes C4 to G8 with the virtual pin when pressing button A and with the direct speaker pin when pressing button B.
We can ignore
playMelody()
as it just converts the musical notation into calls to theplayTone()
function. The frequency and duration sent toplayTone()
are printed to serial.MICROBIT.hex.zip