I2S for PIC32MX/MZ – Application: Music Box

Skill level: Advanced, with C and 32-bit experience.

This tutorial requires an oscilloscope,  a small amplifier and a speaker to confirm the resulting output!

Required: Cytron SK1632, PIC32MX150F128B, MPLAB X IDE, MPLAB XC32 1.40, MPLAB Harmony v1.07 and above.

After shaping up the waves and sending them to the DAC, you are thinking that all it can do is to hear a loud smooth beep from the speakers. Of course, you won’t be just generating sine waves alone – it sounds bland. With this “platform” you have built, it has a lot of room for improvement! Why not “shape” the sine waves to sound a little bit richer?

In this example application, you can do some extra tricks on the sine wave calculation. Remember the DDS algorithm in the past tutorial? One of the extra tricks to increase the timbre of the sound is to use the FM synthesis. You will get to know how it works in this application:

FM Synthesis

If you have listened to the music of the 80s, you get to hear those electronic sound effects which sounds very metallic, harsh and/or artificial in nature. These are done by modulating a sine wave with another sine wave:

f(t) = Acsin(2πfct + Amsin(2πfmt))

  • Ac is amplitude of the carrier.
  • fc  is the frequency of the carrier.
  • Am is amplitude of the modulator.
  • fm is the frequency of the modulator.
  • t is time in seconds.

Basically, you see another sine wave inside the sine wave in the formula. This significantly alters the shape of the sine wave, giving some very unique sounds. Changing the ratios of the frequency of the carrier, fc and the frequency of the modulator, fm, alters the behaviour of the wave. This behaviour is also changed by changing the amplitude of the modulator Am too.

If the Am is zero, the modulator part is cancelled out, leaving this:

f(t) = Acsin(2πfct)

You are then left with the plain sine wave. In order to obtain a more rich sound, the modulator amplitude Am must be more than zero.

In addition, you can watch a bit of the FM synthesis tutorial for your further understanding:

To use this formula with the DDS algorithm is nothing more than just adding a few extra variables for the modulator:

// suffix C means "Carrier", suffix M means "Modulator".
unsigned long accum1_c = 0;
unsigned long tuningWord1_c = 0;
unsigned long accum1_m = 0;
unsigned long tuningWord1_m = 0;
long temp1_m = 0;                              // intermediate for FM synth calculation.
unsigned long amplitude1_m = 2048;             // modulator amplitude.
long output1 = 0;                              // intermediate for channel 1 output

Do note that the FM synthesis requires at least two oscillators (or sine wave generator in this tutorial) to come up with the expected sound quality. That is why you see two tuning words and two accumulators, each for the modulator, and another for the carrier.

The following code generates one (1) FM channel using the variables mentioned earlier:

accum1_m += tuningWord1_m;                  // generating modulator for 1st channel.
temp1_m = (long)wavetable[accum1_m >> 20];
accum1_c += (unsigned long)(tuningWord1_c) + (long)temp1_m*amplitude1_m; // 2085, beta approx. 1.0
output1 = (long)((wavetable[(accum1_c) >> 20] >> 8);

That’s easy. Wait, you are asking “It’s a music box. Only one voice? No multiple voices?”

The answer: Yes, you are going to add them. Else, it would sound very boring. You declare the variables for the second FM channel, of course. Here:

accum2_m += tuningWord2_m;                  // generating modulator for 2nd channel.
temp2_m = (long)wavetable[accum2_m >> 20];
accum2_c += (unsigned long)(tuningWord2_c) + (long)temp2_m*amplitude2_m; // 2085, beta approx. 1.0
output2 = (long)((wavetable[(accum2_c) >> 20] >> 8);

Afterwards, you have to sum up the outputs and it is written in the buffer for output. You have to average it by 2 (since it has two FM channels calculated here) to keep the output value within -32767 and +32767.

 final_output = ( output1 + output2 ) / 2

The activity of adding the other channels (2,3,4,5 and 6) will be an exercise for you.

Attention: If you are using SK1632 with PIC32MX1xx/2xx series, do not use more than 6 channels due to the lower processing power of the processors.

That’s what a music box would sound like. It’s all nice… but it is still missing something? The waves being output would have the constant volume all the time? Naturally, if you have hit a note on a piano (or any other music instrument), the sound comes out loud and then fades away. So to make it even sound more pleasing, the sound being generated by the microcontroller fades away after striking the note.

Envelope generation:

Similarly to the DDS method, a 256-sample table of a decay envelope is used.

To understand the decay envelope, here is a small picture about it:

http://www.integracoustics.com/MUG/MUG/articles/phase/

Source: http://www.integracoustics.com/MUG/MUG/articles/phase/

The yellow portion is the decay envelope, and look how the amplitude of the sine wave slowly decreasing in an exponential manner.

In the following code, there is a little counter (envelope_count1) thats counts up every each sample is generated, and resets to 0 when it has exceeded or equal the ENVELOPE_SPEED value. Afterwards, the envelope_ptr1 (the index number of the envelope table) increases by one each time. If the envelope_ptr1 equals or exceeds the maximum envelope table size (256 samples), it is “saturated” to 255.

// Envelope generator channel 1:
if (envelope_count1 >= ENVELOPE_SPEED) { // decay channel 1
 envelope_count1 = 0;
 if (envelope_ptr1 >= 255)
  envelope_ptr1 = 255;
 else
  envelope_ptr1++;
 } 
  else envelope_count1++;
}

Due to the approximation of the formula y = e-x in the form of table, the resulting output of the envelope value will be very small, so the envelope_ptr1 is saturated to 255 every time, giving the envelope output a value very close to zero. This means the sound is loud at the begining, but slowly decays until it is almost inaudible.

With this envelope now, you multiply the sample of the envelope with the sample of the FM channel:

g(t) = Aenvelope*(Acsin(2πfct + Amsin(2πfmt)))

Aha, now this one sounds like it’s hitting a note in a music instrument.

To sum this all up, you add all the outputs of each FM channel (with each of the FM channel being multiplied by the envelopes) and put it into the buffer:

output1 = (long)((wavetable[(accum1_c) >> 20] >> 8) * envelope[envelope_ptr1]);
output2 = (long)((wavetable[(accum2_c) >> 20] >> 8) * envelope[envelope_ptr2]);
output3 = (long)((wavetable[(accum3_c) >> 20] >> 8) * envelope[envelope_ptr3]);
output4 = (long)((wavetable[(accum4_c) >> 20] >> 8) * envelope[envelope_ptr4]);
output5 = (long)((wavetable[(accum5_c) >> 20] >> 8) * envelope[envelope_ptr5]);
output6 = (long)((wavetable[(accum6_c) >> 20] >> 8) * envelope[envelope_ptr6]);

temp_output1 =  (int)((output1 + output2 + output3 + output4 + output5 + output6) / 16) ;
buffer_pp[2*i]   = temp_output1;    // One DAC channel.
buffer_pp[2*i+1] = temp_output1;    // the Other DAC channel!

In the temp_output1, the total of the outputs from 6 FM channels are divided by 16 instead of 6 is due to controlling the overall volume so that the output remains in the range between -32767 and 32767. Be reminded that the DAC only takes in 16-bit signed values, so exceeding them would give you nasty noise in the output.

Combining it all together: Music Box

This tutorial is all about playing music on the PIC32 microcontroller. Now you are able to play one! We use Len Shustek’s music parser program (source: https://github.com/LenShustek/arduino-playtune) to process the midi files into a simpler C array for the program in the PIC32 to play the music. The parsing is done in the body of the program – it looks very complicated, but you do not need to fully understand the parsing. The purpose of the tutorial is to show you how to effectively use the I2S and the DMA so that you can deploy your audio applications in an effective manner.

Exercise 1: Using the sample program supplied, change the carrier and the modulator’s ratio. What will you hear from it? Hint: CARRIER_AMPL and MOD_AMPL in the defines!

Exercise 2: Grab a midi file of your favourite song and use Len Shustek’s midi tunes software to convert them to a C array form, and replace the output with the one in the songdata.h.

Exercise 3: Play with the modulator’s amplitude on one of the FM channels! Hint: look out for the “unsigned long amplitudeX_m = _____“, where X is the FM channel’s number.

As usual, source code is here: https://github.com/uncle-yong/sk1632-i2s-dma

References:

1.) Getting started with FM synthesis: http://www.synthtopia.com/content/2012/04/14/inside-synthesis-getting-started-with-fm-synthesis/

2.) Computer Music (MUSC 216) FM (Frequency Modulation) Synthesis: http://computermusicresource.com/FM.synthesis.html

 

, , ,

Related Post

FatFS for PIC32MX/MZ

I2S for PIC32MX/MZ – Mikroelektronika Audio Codec Proto Board (Cirrus Logic WM8731 codec)

I2S for PIC32MX/MZ – Direct Memory Access (DMA)

I2S for PIC32MX/MZ – Sine wave generation using DDS

Leave a Reply

Your email address will not be published. Required fields are marked *