Simple Music Maker

Enter Your Electronics & Design Project for Your Chance to Win a Fog Machine and a $100 Shopping Cart!

Back to The Project14 homepage

Project14 Home
Monthly Themes
Monthly Theme Poll

 

Where, like a sweet melodious bird, it sung
Sweet varied notes, enchanting every ear!

 

Marcus Andronicus. Titus Andronicus [III, 1]. William Shakespeare.

 

Previous blogs:

A Simple Arduino Music Box

Simple Arduino Music Box: Voices

Simple Arduino Music Box: Chords

 

Introduction

 

With what I've done so far, the note produced by the music box maintains the same amplitude all the way through the note.

That's quite unrealistic. With a real instrument there's always a period at the start where the sound builds in intensity,

even if it's very fast, and a decline at the end as the energy in the system gradually dissipates. The shape of the

amplitude as it varies over time - what you'd see if you displayed the whole note on an oscilloscope - is called the

envelope. The period at the start, as the sound increases, is called the 'attack' and the period at the end, as it declines,

is the 'decay'.

 

If I want a better sounding note, I need to find a way to generate the envelope. One possibility is to do it in the

software. Another is to do it externally in the hardware. In either case, what we are trying to implement is a multiply -

we'll be multiplying the note values by the value for the envelope. There are some differences between the two approaches,

hardware and software. Doing it in software will result in a dramatic loss in resolution at the lower levels whereas doing

it externally in hardware will preserve the note shape. How much that matters I've got no idea, but doing it in software is

much simpler and easier to try so that's where I'll start.

 

Multiplication

 

Originally, I assumed that the microcontroller wouldn't be able to do a multiply within the time between interrupts -

multiplying with a software library is time consuming -  however, my assumption was wrong - looking at the microcontroller's

datasheet I realised that it has a built-in 8-bit hardware muliplier that can produce a result in only two instruction

cycles.

 

 

The actual instruction I'm trying for is MULSU which multiplies a signed 8-bit value by an unsigned 8-bit value for a signed

16-bit result.

 

 

The signed operand will be the waveform data and the unsigned one will be the envelope value at that moment in time. The

result I'll mangle a bit to give an 8-bit unsigned value to send to the DAC port.

 

First problem, though, is that I'm programming it in C, and not assembler, so how do I make the compiler use the multiply

instruction I want? Is it clever enough to use the instruction when appropriate or will it always use some multiply routine

instead. I'm going to try casting the operands and the result to the appropriate forms and look at the timing to see what is

happening. Much to my surprise, that seems to work - I have an envelope and it still completes easily within the timer

interval.

 

Reworking the Wave Table

 

The second issue is that the waveform data is currently 16-bit unsigned. I could manipulate that, but I've chosen instead to

rework the code that generates the waveform table so that the table contains signed char values that can be directly fed

to the multiply. Here's the code for that.

 

 

 

<stdio.h>

 <conio.h>

 <string.h>

 <dos.h>

 <errno.h>

 <math.h>

 temp_string[256];

 char waveTable[512];

 floatWaveTable[512];


 harmonicTable[10] = {1.0,0.0,0.9,0.0,0.8,0.0,0.0,0.0,0.0,0.0};


 scaleFactor = 1.0;

 maxValue = 0.0;


 main(int argc,char *argv[])

int i,j,temp;

/* print banner */

"\n--- waveTable DOS UTILITY PROGRAM V1.0 ---\n");

"Builds wave table for Arduino Music Box blog.\n");

/* generate float wave table in array */

for(i=0;i<512;i++) {

for(j=0;j<10;j++) {

for(i=0;i<512;i++) {

/* find maximum value and determine scale factor */

for(i=0;i<512;i++) {

if(floatWaveTable[i] > maxValue)

/* convert to int wave table in array */

for(i=0;i<512;i++) {

if( ((floatWaveTable[i] * scaleFactor)) >= 0.0)

unsigned char) ((floatWaveTable[i] * scaleFactor));

else

unsigned char) (floatWaveTable[i] * scaleFactor) + 0xff;

/* open output file */

if((handle=fopen("waveTable.txt","wt"))==NULL) {

"Failed to open output file.\n");

else {

/* write file banner */

"//\n");

"//  --- Wave table - generated by waveTable.exe \n");

"//\n");

"signed char waveTable[512] = {");

/* write table to file */

"\n");

for (i=0;i<32;i++) {

"   ");

for(j=0;j<16;j++) {

"0x%02x",waveTable[(i*16) + j]);

if(j<15)

",");

else {

if(i==31)

"};\n");

else

",\n");

/* close output file */

/* open output .csv file */

if((handle2=fopen("waveTable.csv","wt"))==NULL) {

"Failed to open .csv output file.\n");

else {

/* write table to file */

for (i=0;i<512;i++) {

"%i,%i,\n",i,waveTable[i]);

/* close output file */

"Done.\n");

 

 

One difference now is that if you graph the values with a speadsheet program it will look something like this - the left

half is the positive values (coming up from zero) and the right half is the negative values coming down from the maximum

value (which is one less than zero, ie -1).

 

 

Arduino Sketch

 

Here's the sketch for the Arduino Uno (keep in mind that I'm almost certainly throwing away portability and it might

need a fair bit of work to function on other Arduino platforms). I've changed to Yankee Doodle for the tune -  I figured you

were all probably fed up with Twinkle, Twinkle by now.

 

If your browser doesn't show it properly, the include at the start is of <Arduino.h>

 

/* Arduino Music Box with envelope generation */
#include <Arduino.h>
unsigned char noteState = 0;
unsigned char preScale = 0;
unsigned char envelopeValue = 0;
signed char waveValue = 0;
unsigned int tableOffset = 0;          // wave table index
unsigned int noteTime = 0xc35;         // note duration count
volatile unsigned int tableStep = 0;
int i=0;
unsigned int tuneNotes[] = {
    216,0x8000,
//    
    288,0x8000,
    288,0x8000,
    324,0x8000,
    363,0x4000,
//
    288,0x8000,
    363,0x8000,
    324,0x8000,
    272,0x8000,
//
    288,0x8000,
    288,0x8000,
    324,0x8000,
    363,0x8000,
//
    288,0xffff,
    272,0x8000,
    216,0x4000,
    192,0x4000,
//
    288,0x8000,
    288,0x8000,
    324,0x8000,
    363,0x8000,
//    
    385,0x8000,
    363,0x8000,
    324,0x8000,
    288,0x8000,
//    
    272,0x8000,
    216,0x8000,
    242,0x8000,
    272,0x8000,
//    
    288,0xffff,
    288,0x8000,
//    
    242,0xc000,
    272,0x4000,
    242,0x8000,
    216,0x8000,
//    
    242,0x8000,
    272,0x8000,
    288,0xffff,
//    
    216,0xc000,
    242,0x4000,
    216,0x8000,
    192,0x8000,
//    
    182,0x8000,
    192,0x8000,
    216,0xffff,
    
    0,0
    };
//
//  --- Wave table - generated by waveTable.exe 
//
signed char waveTable[512] = {
   0x00,0x06,0x0c,0x12,0x18,0x1e,0x24,0x2a,0x2f,0x35,0x3a,0x40,0x45,0x4a,0x4f,0x53,
   0x58,0x5c,0x60,0x64,0x67,0x6b,0x6e,0x71,0x73,0x75,0x78,0x79,0x7b,0x7c,0x7d,0x7e,
   0x7e,0x7e,0x7e,0x7e,0x7e,0x7d,0x7c,0x7b,0x79,0x78,0x76,0x74,0x71,0x6f,0x6c,0x6a,
   0x67,0x64,0x61,0x5e,0x5b,0x57,0x54,0x51,0x4d,0x4a,0x46,0x43,0x3f,0x3c,0x39,0x35,
   0x32,0x2f,0x2c,0x29,0x26,0x23,0x20,0x1e,0x1c,0x19,0x17,0x15,0x13,0x12,0x10,0x0f,
   0x0e,0x0d,0x0c,0x0c,0x0b,0x0b,0x0b,0x0b,0x0b,0x0b,0x0c,0x0c,0x0d,0x0e,0x0f,0x10,
   0x11,0x13,0x14,0x15,0x17,0x19,0x1a,0x1c,0x1e,0x1f,0x21,0x23,0x25,0x26,0x28,0x2a,
   0x2b,0x2d,0x2f,0x30,0x31,0x33,0x34,0x35,0x36,0x37,0x38,0x38,0x39,0x39,0x3a,0x3a,
   0x3a,0x3a,0x3a,0x39,0x39,0x38,0x38,0x37,0x36,0x35,0x34,0x33,0x31,0x30,0x2f,0x2d,
   0x2b,0x2a,0x28,0x26,0x25,0x23,0x21,0x1f,0x1e,0x1c,0x1a,0x19,0x17,0x15,0x14,0x13,
   0x11,0x10,0x0f,0x0e,0x0d,0x0c,0x0c,0x0b,0x0b,0x0b,0x0b,0x0b,0x0b,0x0c,0x0c,0x0d,
   0x0e,0x0f,0x10,0x12,0x13,0x15,0x17,0x19,0x1c,0x1e,0x20,0x23,0x26,0x29,0x2c,0x2f,
   0x32,0x35,0x39,0x3c,0x3f,0x43,0x46,0x4a,0x4d,0x51,0x54,0x57,0x5b,0x5e,0x61,0x64,
   0x67,0x6a,0x6c,0x6f,0x71,0x74,0x76,0x78,0x79,0x7b,0x7c,0x7d,0x7e,0x7e,0x7e,0x7f,
   0x7e,0x7e,0x7d,0x7c,0x7b,0x79,0x78,0x75,0x73,0x71,0x6e,0x6b,0x67,0x64,0x60,0x5c,
   0x58,0x53,0x4f,0x4a,0x45,0x40,0x3a,0x35,0x2f,0x2a,0x24,0x1e,0x18,0x12,0x0c,0x06,
   0x00,0xf9,0xf3,0xed,0xe7,0xe1,0xdb,0xd5,0xd0,0xca,0xc5,0xbf,0xba,0xb5,0xb0,0xac,
   0xa7,0xa3,0x9f,0x9b,0x98,0x94,0x91,0x8e,0x8c,0x8a,0x87,0x86,0x84,0x83,0x82,0x81,
   0x81,0x81,0x81,0x81,0x81,0x82,0x83,0x84,0x86,0x87,0x89,0x8b,0x8e,0x90,0x93,0x95,
   0x98,0x9b,0x9e,0xa1,0xa4,0xa8,0xab,0xae,0xb2,0xb5,0xb9,0xbc,0xc0,0xc3,0xc6,0xca,
   0xcd,0xd0,0xd3,0xd6,0xd9,0xdc,0xdf,0xe1,0xe3,0xe6,0xe8,0xea,0xec,0xed,0xef,0xf0,
   0xf1,0xf2,0xf3,0xf3,0xf4,0xf4,0xf4,0xf4,0xf4,0xf4,0xf3,0xf3,0xf2,0xf1,0xf0,0xef,
   0xee,0xec,0xeb,0xea,0xe8,0xe6,0xe5,0xe3,0xe1,0xe0,0xde,0xdc,0xda,0xd9,0xd7,0xd5,
   0xd4,0xd2,0xd0,0xcf,0xce,0xcc,0xcb,0xca,0xc9,0xc8,0xc7,0xc7,0xc6,0xc6,0xc5,0xc5,
   0xc5,0xc5,0xc5,0xc6,0xc6,0xc7,0xc7,0xc8,0xc9,0xca,0xcb,0xcc,0xce,0xcf,0xd0,0xd2,
   0xd4,0xd5,0xd7,0xd9,0xda,0xdc,0xde,0xe0,0xe1,0xe3,0xe5,0xe6,0xe8,0xea,0xeb,0xec,
   0xee,0xef,0xf0,0xf1,0xf2,0xf3,0xf3,0xf4,0xf4,0xf4,0xf4,0xf4,0xf4,0xf3,0xf3,0xf2,
   0xf1,0xf0,0xef,0xed,0xec,0xea,0xe8,0xe6,0xe3,0xe1,0xdf,0xdc,0xd9,0xd6,0xd3,0xd0,
   0xcd,0xca,0xc6,0xc3,0xc0,0xbc,0xb9,0xb5,0xb2,0xae,0xab,0xa8,0xa4,0xa1,0x9e,0x9b,
   0x98,0x95,0x93,0x90,0x8e,0x8b,0x89,0x87,0x86,0x84,0x83,0x82,0x81,0x81,0x81,0x80,
   0x81,0x81,0x82,0x83,0x84,0x86,0x87,0x8a,0x8c,0x8e,0x91,0x94,0x98,0x9b,0x9f,0xa3,
   0xa7,0xac,0xb0,0xb5,0xba,0xbf,0xc5,0xca,0xd0,0xd5,0xdb,0xe1,0xe7,0xed,0xf3,0xf9};
void setup() {
  
  // set the digital pins as outputs:
  pinMode(0, OUTPUT);
  pinMode(1, OUTPUT);
  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, OUTPUT);
  pinMode(6, OUTPUT);
  pinMode(7, OUTPUT);
 
  pinMode(8, OUTPUT);     // for debug
  // timer 2 set up
  cli();                  // disable interrupts
  TCCR2A = 0;             // control register all 0
  TCCR2B = 0;             // control register all 0
  TCNT2 = 0;              // set count to 0
  OCR2A = 159;            // period = 160 x 1/16MHz = 10uS
  TCCR2A |= (1 << WGM21); // mode is clear on match
  TCCR2B |= (1 << CS20);  // no prescaler   
  TIMSK2 |= (1 << OCIE2A);  // enable interrupt on match
  sei();                  // enable interrupts
}
ISR(TIMER2_COMPA_vect) {
  PORTB |= 0x01;
  switch(noteState) {
    case 0:                           // inter-note gap
      PORTD = 0x80;
      noteTime = noteTime - 1;
      if(noteTime==0) {
        tableStep = tuneNotes[i++];
        noteTime = tuneNotes[i++] - 0x16e9;
        tableOffset = 0;
        envelopeValue = 0;
        preScale = 3;
        noteState = 1;
        }
      break;
    case 1:                           // attack
      tableOffset = tableOffset + tableStep;
      waveValue = (waveTable[tableOffset >> 7]);
      PORTD = (((signed int)(waveValue * envelopeValue)) >> 8) + 0x80;
      preScale--;
      if(preScale==0) {
        preScale = 3;
        if(envelopeValue != 255) {
          envelopeValue++;
          }
        else {
          noteState = 2;
          }
        }
      break;
    case 2:                           // hold
      tableOffset = tableOffset + tableStep;
      waveValue = (waveTable[tableOffset >> 7]);
      PORTD = (((signed int)(waveValue * envelopeValue)) >> 8) + 0x80;
      if(noteTime==0) {
        preScale = 20;
        noteState = 3;
        }
      else {
        noteTime = noteTime - 1;
        }
      break;
    case 3:                           // decay
      tableOffset = tableOffset + tableStep;
      waveValue = (waveTable[tableOffset >> 7]);
      PORTD = (((signed int)(waveValue * envelopeValue)) >> 8) + 0x80;
      preScale--;
      if(preScale==0) {
        preScale = 20;
        if(envelopeValue != 0) {
          envelopeValue--;
          }
        else {
          tableOffset = 0;
          tableStep = 0;
          noteTime = 0xc35;
          if (tuneNotes[i]==0)
            i=0;
          noteState = 0;
          }
        }
      break;
    }
         
  PORTB &= 0xFE; 
}
void loop() {
}

 

Waveforms

 

Here's a scope trace showing the end of one note and the start of the next:

 

 

Here's the start (the 'attack') in more detail.

 

 

Here's the end (the 'decay') in more detail. That's fine too. So, it looks like I've gotten all the signs and the

casting right.

 

 

My envelope generator isn't very sophisticated - just a ramp up and a ramp down - but I think you can probably see that the

values could come out of a table and give any envelope shape you wanted. I'll leave that as 'an exercise for the reader' (as

all the lazy writers say).

 

The Envelope Generator in Action

 

Finally here's a performance of part of Yankee Doodle (with some wrong notes - you'll have to correct them yourself in the

sketch and finish it off if you want to use it) played on the amazing £2 Arduino Music Box (might be a little more if you don't have an old

loudspeaker you can repurpose). My assistant still isn't impressed - this time he's got the rocket ready for a quick getaway

in case it all gets too much.

 

 

 

 

There's still a bit too much noise. That results from variety of sources. Firstly, there's noise from the sample reconstruction.

That comes from working only 8 bits, the imperfect filtering, and the fact that there's another interrupt going on that's throwing

the position of the samples around. There's also a bit of noise resulting from the crossover distortion of my crude and

somewhat simple amplifier. Finally, the loudspeaker isn't very good - the frequency response isn't very good, there's a

horrible resonance on one of the notes, and there's a slight scratchy sound if I move the cone back and forwards which suggests

that it's seen much better days - I'll have a search and see if I can locate a better one.

 

Next blog:

Simple Arduino Music Box: Chimes