Enter Your Electronics & Design Project for Your Chance to Win a Fog Machine and a $100 Shopping Cart! | 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:
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:
Top Comments