Introduction
Happy Birthday Arduino and Happy Birthday Project14.
My entry to the Open Arduino competition was going to be a music box. I'm too late for that, having missed the deadline, so I'll post the blog in Simple Music Maker instead (and keep my fingers crossed that I don't win a fog machine). As you can hear, it's far from perfect, but it was fun to put together and play with.
The Uno is going to synthesize the sounds digitally. There's a small amount of analogue circuitry to convert the digital values to an analogue signal and drive a small loudspeaker with a very simple amplifier.
Circuit description
The waveform is generated by a R-2R network working as a DAC (digital-to-analog converter) directly from the Arduino port pins.
See this series of blogs for more on the R-2R network used as a DAC: Arduino: R-2R Experiment
By its nature, because the digital I/O pins swing between the rails, the output of the DAC is 0 to 5V fullscale. That's too high for what I want here, so I need to reduce the swing to something more manageable. I do that with the first stage (U1) which is an inverting amplifier with a gain of one tenth fifth - that then gets me a maximum swing of half a volt (I changed it after writing this because the original was a bit quiet and then forgot to come back and edit the text). The gain is simply R1/R8. The idea behind the 150pF capacitor is to slow the edges of the DAC output which are very fast and cause the op-amp to ring - it helps, but could be done better. The LM324 is an old bipolar op-amp that can't work up all the way up to the supply rail, and it's probably best to keep everything well clear of ground too, so the first stage also places the signal around 2V. It doesn't matter what the actual, precise level is as long as the signals are in a range where the op-amp can function properly - the audio is ac and at the end of the chain will pass through a capacitor, after which it will be referenced to ground again, so we don't care about arbitrary dc offsets prior to that.
Following the attenuator, there's a low-pass filter which comprises op-amp U4 and the passive components around it. It's a third-order Sallen-Key filter. This smoothes the waveform and removes the steps at the sampling rate. I didn't design this from scratch, instead I copied an existing design and scaled the resistor values to modify the cut-off frequency. The op-amp is configured as a follower and there's a dc path from the previous stage to the non-inverting input, so the output will still be centred about 2V.
The final stage, comprising U2 and the transistors, is a simple (and somewhat crude) power amplifier. The op-amp can't manage enough current to directly drive an 8 ohm loudspeaker, so the transistors lend a hand. The transistors are configured as emitter followers in a push-pull arrangement. To increase the output voltage the op-amp lifts the base of T1, T2 will naturally be off in this situation, and to decrease the output voltage, the op-amp lowers the base voltage of T2, where T1 will be off. The two diodes drop slightly less than the Vbe drops of the transistors and reduce the distance that the op-amp output has to swing when it changes over from one transistor to the other - that reduces the distortion that would otherwise occur because of the slew rate of the op-amp limiting the rate at which its output can change. The whole output stage is within the feedback loop of the op-amp and because it's wired as a follower the voltage at the loudspeaker should be the same as the filter output. There's still a little crossover distortion - a designer of hi-fi power amplifiers would die of shame if their amplifier has these kind of specs - but for a simple Arduino project it's fine.
This is what the simulator shows for the overall frequency response:
The output capacitor is there to give a signal at the loudspeaker that swings about ground and gives an avarage of zero - it's not good to have a permanent bias on a loudspeaker. The capacitor forms a high-pass filter with the load resistance (8 ohms) and for the cut-off frequency to be reasonable it needs to be a fairly high value. The 470u here gives a cut-off of about 100Hz. Not Hi-Fi quality, but I don't suppose anyone will worry about that - this isn't intended as a high-fidelity music box.
Build
As with the R-2R experiments, I built this one on a piece of tri-pad board.
Here's the board as I built it:
Testing
The starting point for the testing was to simply use the sketches I'd done previously for the audio generator experiments.
Here are some traces from the board.
This shows a sweep from 20Hz to 20kHz and you can see the way the filter slopes off from about 10kHz upwards:
This is the waveform at the loudspeaker terminals (440Hz) - it's kind of sine-like. The little steps on the slopes of the sinewave are the points where the op-amp is frantically trying to get its output to the right place to effect the changeover between the two transistors.
Music Box Sketch
Finally, here's the sketch to play the tune (the include at the start is of arduino.h):
/* Arduino Music Box */ #include 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[] = { 343,0x61c5, 385,0x61bf, 343,0x61c5, 385,0x61bf, 343,0x61c5, 385,0x61bf, 343,0xc38b, 343,0x61c5, 306,0x61e2, 288,0x61c7, 229,0x61d1, 192,0x6155, 306,0x6177, 288,0xc38e, 171,0x620f, 192,0x6155, 229,0x61d1, 257,0x619e, 288,0x61c7, 306,0x61e2, 343,0xc38b, 343,0x61c5, 288,0x61c7, 229,0x61d1, 306,0x61e2, 257,0x619e, 257,0x619e, 229,0xc3a2, 0,0 }; // // --- Sine table - generated by sineTable.exe // unsigned int sineTable[512] = { 0x8000,0x8192,0x8324,0x84b6,0x8647,0x87d9,0x896a,0x8afb,0x8c8b,0x8e1b,0x8fab,0x9139,0x92c8,0x9455,0x95e2,0x976d, 0x98f8,0x9a82,0x9c0b,0x9d93,0x9f19,0xa09f,0xa223,0xa3a6,0xa528,0xa6a8,0xa826,0xa9a3,0xab1f,0xac98,0xae11,0xaf87, 0xb0fb,0xb26e,0xb3de,0xb54d,0xb6ba,0xb824,0xb98c,0xbaf2,0xbc56,0xbdb8,0xbf17,0xc073,0xc1ce,0xc325,0xc47a,0xc5cd, 0xc71c,0xc869,0xc9b4,0xcafb,0xcc3f,0xcd81,0xcebf,0xcffb,0xd133,0xd269,0xd39b,0xd4ca,0xd5f5,0xd71d,0xd842,0xd964, 0xda82,0xdb9d,0xdcb4,0xddc7,0xded7,0xdfe3,0xe0ec,0xe1f1,0xe2f2,0xe3ef,0xe4e8,0xe5dd,0xe6cf,0xe7bd,0xe8a6,0xe98c, 0xea6d,0xeb4a,0xec24,0xecf9,0xedca,0xee96,0xef5f,0xf023,0xf0e2,0xf19e,0xf255,0xf307,0xf3b5,0xf45f,0xf504,0xf5a5, 0xf641,0xf6d9,0xf76c,0xf7fa,0xf884,0xf909,0xf98a,0xfa05,0xfa7d,0xfaef,0xfb5d,0xfbc5,0xfc29,0xfc89,0xfce3,0xfd39, 0xfd8a,0xfdd6,0xfe1d,0xfe5f,0xfe9d,0xfed5,0xff09,0xff38,0xff62,0xff87,0xffa7,0xffc2,0xffd8,0xffe9,0xfff6,0xfffd, 0xffff,0xfffd,0xfff6,0xffe9,0xffd8,0xffc2,0xffa7,0xff87,0xff62,0xff38,0xff09,0xfed5,0xfe9d,0xfe5f,0xfe1d,0xfdd6, 0xfd8a,0xfd39,0xfce3,0xfc89,0xfc29,0xfbc5,0xfb5d,0xfaef,0xfa7d,0xfa05,0xf98a,0xf909,0xf884,0xf7fa,0xf76c,0xf6d9, 0xf641,0xf5a5,0xf504,0xf45f,0xf3b5,0xf307,0xf255,0xf19e,0xf0e2,0xf023,0xef5f,0xee96,0xedca,0xecf9,0xec24,0xeb4a, 0xea6d,0xe98c,0xe8a6,0xe7bd,0xe6cf,0xe5dd,0xe4e8,0xe3ef,0xe2f2,0xe1f1,0xe0ec,0xdfe3,0xded7,0xddc7,0xdcb4,0xdb9d, 0xda82,0xd964,0xd842,0xd71d,0xd5f5,0xd4ca,0xd39b,0xd269,0xd133,0xcffb,0xcebf,0xcd81,0xcc3f,0xcafb,0xc9b4,0xc869, 0xc71c,0xc5cd,0xc47a,0xc325,0xc1ce,0xc073,0xbf17,0xbdb8,0xbc56,0xbaf2,0xb98c,0xb824,0xb6ba,0xb54d,0xb3de,0xb26e, 0xb0fb,0xaf87,0xae11,0xac98,0xab1f,0xa9a3,0xa826,0xa6a8,0xa528,0xa3a6,0xa223,0xa09f,0x9f19,0x9d93,0x9c0b,0x9a82, 0x98f8,0x976d,0x95e2,0x9455,0x92c8,0x9139,0x8fab,0x8e1b,0x8c8b,0x8afb,0x896a,0x87d9,0x8647,0x84b6,0x8324,0x8192, 0x8000,0x7e6d,0x7cdb,0x7b49,0x79b8,0x7826,0x7695,0x7504,0x7374,0x71e4,0x7054,0x6ec6,0x6d37,0x6baa,0x6a1d,0x6892, 0x6707,0x657d,0x63f4,0x626c,0x60e6,0x5f60,0x5ddc,0x5c59,0x5ad7,0x5957,0x57d9,0x565c,0x54e0,0x5367,0x51ee,0x5078, 0x4f04,0x4d91,0x4c21,0x4ab2,0x4945,0x47db,0x4673,0x450d,0x43a9,0x4247,0x40e8,0x3f8c,0x3e31,0x3cda,0x3b85,0x3a32, 0x38e3,0x3796,0x364b,0x3504,0x33c0,0x327e,0x3140,0x3004,0x2ecc,0x2d96,0x2c64,0x2b35,0x2a0a,0x28e2,0x27bd,0x269b, 0x257d,0x2462,0x234b,0x2238,0x2128,0x201c,0x1f13,0x1e0f,0x1d0d,0x1c10,0x1b17,0x1a22,0x1930,0x1842,0x1759,0x1673, 0x1592,0x14b5,0x13db,0x1306,0x1235,0x1169,0x10a0,0x0fdc,0x0f1d,0x0e61,0x0daa,0x0cf8,0x0c4a,0x0ba0,0x0afb,0x0a5a, 0x09be,0x0926,0x0893,0x0805,0x077b,0x06f6,0x0675,0x05fa,0x0582,0x0510,0x04a2,0x043a,0x03d6,0x0376,0x031c,0x02c6, 0x0275,0x0229,0x01e2,0x01a0,0x0162,0x012a,0x00f6,0x00c7,0x009d,0x0078,0x0058,0x003d,0x0027,0x0016,0x0009,0x0002, 0x0000,0x0002,0x0009,0x0016,0x0027,0x003d,0x0058,0x0078,0x009d,0x00c7,0x00f6,0x012a,0x0162,0x01a0,0x01e2,0x0229, 0x0275,0x02c6,0x031c,0x0376,0x03d6,0x043a,0x04a2,0x0510,0x0582,0x05fa,0x0675,0x06f6,0x077b,0x0805,0x0893,0x0926, 0x09be,0x0a5a,0x0afb,0x0ba0,0x0c4a,0x0cf8,0x0daa,0x0e61,0x0f1d,0x0fdc,0x10a0,0x1169,0x1235,0x1306,0x13db,0x14b5, 0x1592,0x1673,0x1759,0x1842,0x1930,0x1a22,0x1b17,0x1c10,0x1d0d,0x1e0e,0x1f13,0x201c,0x2128,0x2238,0x234b,0x2462, 0x257d,0x269b,0x27bd,0x28e2,0x2a0a,0x2b35,0x2c64,0x2d96,0x2ecc,0x3004,0x3140,0x327e,0x33c0,0x3504,0x364b,0x3796, 0x38e3,0x3a32,0x3b85,0x3cda,0x3e31,0x3f8c,0x40e8,0x4247,0x43a9,0x450d,0x4673,0x47db,0x4945,0x4ab2,0x4c21,0x4d91, 0x4f04,0x5078,0x51ee,0x5367,0x54e0,0x565c,0x57d9,0x5957,0x5ad7,0x5c59,0x5ddc,0x5f60,0x60e6,0x626c,0x63f4,0x657d, 0x6707,0x6892,0x6a1d,0x6baa,0x6d37,0x6ec6,0x7054,0x71e4,0x7374,0x7504,0x7695,0x7826,0x79b8,0x7b49,0x7cdb,0x7e6d}; 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); // 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) { if(tableStep == 0) { PORTD = 0x80; noteTime = noteTime - 1; if(noteTime==0) { tableStep = tuneNotes[i++]; noteTime = tuneNotes[i++]; tableOffset = 0; } } else { tableOffset = tableOffset + tableStep; PORTD = (sineTable[tableOffset >> 7]) >> 8; noteTime = noteTime - 1; if(noteTime==0) { tableOffset = 0; tableStep = 0; noteTime = 0xc35; if (tuneNotes[i]==0) i=0; } } } void loop() { }
The code is a bit clunky but it's a starting point for experimenting with. This is a rather fast version of 'Twinkle, Twinkle, Little Star' (I know it doesn't sound much like it, but really it is). This is with the waveform table containing a sine wave, so it has an electronic quality to it, and there's no envelope control, so the notes just slam in and out. The note table is arranged in pairs, first the note and then the duration. The note is the step value for the waveform table, the duration is the number of 10uS intervals. Having the duration in terms of the steps might seem awkward (and it is) but when I first tried it I got a lot of nasty clicking from having abrupt terminations part way through a sine wave cycle, so these values have been arranged to finish close to the point where it would cross zero.
For an actual music box, it would probably want to play through once on being powered up and then turn itself off - at the moment it repeats endlessly.
There are follow-on blogs here:
Simple Arduino Music Box: Voices
Simple Arduino Music Box: Chords
Top Comments