Like sweet bells jangled, out of tune and harsh;
Ophelia, Hamlet [III, 1], William Shakespeare
Previous blogs:
Simple Arduino Music Box: Voices
Simple Arduino Music Box: Chords
Simple Arduino Music Box: Envelopes
Introduction
I'm discovering that, if you build a simple, programmable device to make sound, you can't stop playing with it. With each of these blogs I say to myself that it's going to be the last, and yet here I am doing another one.
The original project was supposed to be a music box, yet all the sounds I've created so far sound more like an electronic organ than a traditional music box. This blog is an attempt to make a sound that has more of the quality of a chime or bell, or at the very least something metallic and tinkly. It's not all that successful, though the resulting sound has some of the qualities I was after. I've discovered, in doing these blogs, that creating imitations of real sounds is very difficult.
Instruments based on a column of air or a vibrating string naturally produce overtones that are related in a simple way to the fundamental (harmonics) [though that doesn't mean that there aren't other, more complex, components to the sound]. With other kinds of instruments things are immediately more complex. Bells, chimes, triangles, and other percussive instruments are 3-D structures and support many resonant modes which aren't necessarily related. So, if I want such a sound, I need to move away from simple harmonics and consider partials (overtones without an integer relationship to a fundamental). I can't get that from manipulating the waveshape of a single cycle, in the way that I did for harmonics, instead I have to treat them as separate sounds like I did with chords. I'm going to have to generate them separately and that's going to leave me very, very short of time in the interrupt routine - indeed, I know from what I did before that it won't run in the time so, to stop it over-running, I'm going to extend the timer period to reduce the sample rate to 62,500sps (15uS for the period between interrupts).
My starting point was some measured partials that I found in a book on acoustics by Benade[1] (thank you, Colchester Public Library). They were for a clock chime which was intended to mimic a bell.
P = 5-10cps (inaudible)
Q = 180cps (1)
Ra = 525cps (10)
Rb = 530cps (6.3)
S = 1063cps (22)
T = 1771cps (45)
He labels his overtones with letters starting at 'P'. The number in brackets is the intensity relative to Q, so you can see that the higher partials are considerably louder than the lower ones, and the lowest is so low in frequency that it can't be heard.
I don't have time to generate all those partials, so as an experiment I'm going to choose four [the top four] and see how it sounds. I also don't have the dynamic range (with only 8 bits) nor the processing time to do the maths to scale the relative amplitudes properly, so it's going to be a very rough and ready approximation. But the great thing about experimenting with music like this is that once the hardware platform is built it becomes very easy and quick to try things out and just see how they sound (and if it sounds good, it is good, irrespective of whether it's the right way to do something). To get the different notes I simply transpose the frequencies of all of the partials by 2^(1/12) for each semitone in the usual way for an equally-tempered scale. I've also moved everything up an octave by multiplying all the frequencies by two.
For the envelope, I'm going for a fast attack and a long decay, with no hold in between - an envelope shape that's common for percussive instruments where all the energy driving the system goes in quickly at the start and then steadily dissipates.
The Hardware
The hardware is still the same as for the initial Simple Music Box, just a simple DAC, a filter, and a crude amplifier to drive a loudspeaker.
The Sketch
Here's the experimental Arduino Uno sketch. It works but it's not neat and tidy code - for instance, part of the state machine is skipped because I don't need the hold period. As before, it's specific to the Uno and may need to be reworked for a different platform.
/* Arduino Music Box */ /* chimes by additive synthesis of partials */ #include <Arduino.h> unsigned char noteState = 0; unsigned char preScale = 0; unsigned char envelopeValue = 0; unsigned int noteTime = 0xc35; // note duration count unsigned int tableStep1 = 0; // wave table step size unsigned int tableStep2 = 0; unsigned int tableStep3 = 0; unsigned int tableStep4 = 0; unsigned int tableOffset1 = 0; // wave table index unsigned int tableOffset2 = 0; unsigned int tableOffset3 = 0; unsigned int tableOffset4 = 0; signed char waveValue1 = 0; // value read from table signed char waveValue2 = 0; signed char waveValue3 = 0; signed char waveValue4 = 0; signed char summedValue = 0; int i=0; // 1101,1111,2229,3716,0x0010, //C // 1166,1178,2362,3937,0x0010, //C# // 1236,1248,2502,4171,0x0010, //D // 1309,1322,2651,4419,0x0010, //Eb // 1387,1400,2809,4682,0x0010, //E // 1470,1484,2976,4960,0x0010, //F // 1557,1572,3153,5255,0x0010, //F# // 1650,1665,3340,5568,0x0010, //G // 1748,1764,3539,5899,0x0010, //G# // 1852,1869,3749,6250,0x0010, //A // 1962,1980,3972,6621,0x0010, //Bb // 2078,2098,4208,7015,0x0010, //B unsigned int tuneNotes[] = { 1387,1400,2809,4682,0x0010, //E 1852,1869,3749,6250,0x0010, //A 1852,1869,3749,6250,0x0010, //A 2078,2098,4208,7015,0x0010, //B 1166,1178,2362,3937,0x0010, //C# 1852,1869,3749,6250,0x0010, //A 1166,1178,2362,3937,0x0010, //C# 2078,2098,4208,7015,0x0010, //B 1748,1764,3539,5899,0x0010, //G# 1852,1869,3749,6250,0x0010, //A 1852,1869,3749,6250,0x0010, //A 2078,2098,4208,7015,0x0010, //B 1166,1178,2362,3937,0x0010, //C# 1852,1869,3749,6250,0x0010, //A 1852,1869,3749,6250,0x0010, //A 1748,1764,3539,5899,0x0010, //G# 1387,1400,2809,4682,0x0010, //E 1852,1869,3749,6250,0x0010, //A 1852,1869,3749,6250,0x0010, //A 2078,2098,4208,7015,0x0010, //B 1166,1178,2362,3937,0x0010, //C# 1236,1248,2502,4171,0x0010, //D 1166,1178,2362,3937,0x0010, //C# 2078,2098,4208,7015,0x0010, //B 1852,1869,3749,6250,0x0010, //A 1748,1764,3539,5899,0x0010, //G# 1387,1400,2809,4682,0x0010, //E 1557,1572,3153,5255,0x0010, //F# 1748,1764,3539,5899,0x0010, //G# 1852,1869,3749,6250,0x0010, //A 1852,1869,3749,6250,0xc000, //A 0,0,0,0 }; // // --- Wave table - generated by waveTable.exe // signed char waveTable[512] = { 0x00,0x04,0x08,0x0c,0x10,0x14,0x18,0x1c,0x20,0x24,0x28,0x2c,0x30,0x34,0x38,0x3b, 0x3f,0x42,0x46,0x49,0x4d,0x50,0x53,0x56,0x59,0x5c,0x5e,0x61,0x64,0x66,0x68,0x6b, 0x6d,0x6f,0x70,0x72,0x74,0x75,0x77,0x78,0x79,0x7a,0x7b,0x7c,0x7d,0x7d,0x7e,0x7e, 0x7e,0x7e,0x7f,0x7e,0x7e,0x7e,0x7e,0x7d,0x7c,0x7c,0x7b,0x7a,0x79,0x78,0x77,0x76, 0x74,0x73,0x72,0x70,0x6f,0x6d,0x6b,0x6a,0x68,0x66,0x64,0x63,0x61,0x5f,0x5d,0x5b, 0x59,0x57,0x55,0x53,0x51,0x4f,0x4d,0x4b,0x49,0x47,0x45,0x43,0x41,0x3f,0x3d,0x3b, 0x3a,0x38,0x36,0x34,0x33,0x31,0x30,0x2e,0x2d,0x2b,0x2a,0x28,0x27,0x26,0x25,0x23, 0x22,0x21,0x20,0x1f,0x1f,0x1e,0x1d,0x1c,0x1b,0x1b,0x1a,0x1a,0x19,0x19,0x18,0x18, 0x18,0x17,0x17,0x17,0x17,0x17,0x17,0x17,0x16,0x16,0x16,0x17,0x17,0x17,0x17,0x17, 0x17,0x17,0x17,0x17,0x18,0x18,0x18,0x18,0x18,0x18,0x19,0x19,0x19,0x19,0x19,0x19, 0x19,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x1a,0x19,0x19,0x19, 0x19,0x19,0x19,0x18,0x18,0x18,0x18,0x17,0x17,0x17,0x16,0x16,0x15,0x15,0x14,0x14, 0x14,0x13,0x13,0x12,0x12,0x11,0x11,0x10,0x0f,0x0f,0x0e,0x0e,0x0d,0x0d,0x0c,0x0c, 0x0b,0x0b,0x0a,0x0a,0x09,0x09,0x08,0x08,0x07,0x07,0x06,0x06,0x05,0x05,0x05,0x04, 0x04,0x03,0x03,0x03,0x03,0x02,0x02,0x02,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, 0xff,0xff,0xff,0xfe,0xfe,0xfe,0xfe,0xfe,0xfe,0xfd,0xfd,0xfd,0xfc,0xfc,0xfc,0xfc, 0xfb,0xfb,0xfa,0xfa,0xfa,0xf9,0xf9,0xf8,0xf8,0xf7,0xf7,0xf6,0xf6,0xf5,0xf5,0xf4, 0xf4,0xf3,0xf3,0xf2,0xf2,0xf1,0xf1,0xf0,0xf0,0xef,0xee,0xee,0xed,0xed,0xec,0xec, 0xeb,0xeb,0xeb,0xea,0xea,0xe9,0xe9,0xe8,0xe8,0xe8,0xe7,0xe7,0xe7,0xe7,0xe6,0xe6, 0xe6,0xe6,0xe6,0xe6,0xe5,0xe5,0xe5,0xe5,0xe5,0xe5,0xe5,0xe5,0xe5,0xe5,0xe5,0xe5, 0xe6,0xe6,0xe6,0xe6,0xe6,0xe6,0xe6,0xe7,0xe7,0xe7,0xe7,0xe7,0xe7,0xe8,0xe8,0xe8, 0xe8,0xe8,0xe8,0xe8,0xe8,0xe8,0xe9,0xe9,0xe9,0xe8,0xe8,0xe8,0xe8,0xe8,0xe8,0xe8, 0xe7,0xe7,0xe7,0xe6,0xe6,0xe5,0xe5,0xe4,0xe4,0xe3,0xe2,0xe1,0xe0,0xe0,0xdf,0xde, 0xdd,0xdc,0xda,0xd9,0xd8,0xd7,0xd5,0xd4,0xd2,0xd1,0xcf,0xce,0xcc,0xcb,0xc9,0xc7, 0xc5,0xc4,0xc2,0xc0,0xbe,0xbc,0xba,0xb8,0xb6,0xb4,0xb2,0xb0,0xae,0xac,0xaa,0xa8, 0xa6,0xa4,0xa2,0xa0,0x9e,0x9c,0x9b,0x99,0x97,0x95,0x94,0x92,0x90,0x8f,0x8d,0x8c, 0x8b,0x89,0x88,0x87,0x86,0x85,0x84,0x83,0x83,0x82,0x81,0x81,0x81,0x81,0x80,0x81, 0x81,0x81,0x81,0x82,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x8a,0x8b,0x8d,0x8f,0x90, 0x92,0x94,0x97,0x99,0x9b,0x9e,0xa1,0xa3,0xa6,0xa9,0xac,0xaf,0xb2,0xb6,0xb9,0xbd, 0xc0,0xc4,0xc7,0xcb,0xcf,0xd3,0xd7,0xdb,0xdf,0xe3,0xe7,0xeb,0xef,0xf3,0xf7,0xfb}; 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 = 255; // 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) { tableStep1 = tuneNotes[i++]; tableStep2 = tuneNotes[i++]; tableStep3 = tuneNotes[i++]; tableStep4 = tuneNotes[i++]; noteTime = tuneNotes[i++]; tableOffset1 = 0; tableOffset2 = 0; tableOffset3 = 0; tableOffset4 = 0; envelopeValue = 0; preScale = 3; noteState = 1; } break; case 1: // attack tableOffset1 = tableOffset1 + tableStep1; tableOffset2 = tableOffset2 + tableStep2; tableOffset3 = tableOffset3 + tableStep3; tableOffset4 = tableOffset4 + tableStep4; waveValue1 = (waveTable[tableOffset1 >> 7]) >> 4; waveValue2 = (waveTable[tableOffset2 >> 7]) >> 4; waveValue3 = (waveTable[tableOffset3 >> 7]) >> 2; waveValue4 = (waveTable[tableOffset4 >> 7]) >> 1; summedValue = waveValue1 + waveValue2 + waveValue3 + waveValue4; PORTD = (((signed int)(summedValue * envelopeValue)) >> 8) + 0x80; preScale--; if(preScale==0) { preScale = 2; if(envelopeValue != 255) { envelopeValue++; } else { noteState = 3; } } break; case 2: // hold tableOffset1 = tableOffset1 + tableStep1; tableOffset2 = tableOffset2 + tableStep2; tableOffset3 = tableOffset3 + tableStep3; tableOffset4 = tableOffset4 + tableStep4; waveValue1 = (waveTable[tableOffset1 >> 7]) >> 4; waveValue2 = (waveTable[tableOffset2 >> 7]) >> 4; waveValue3 = (waveTable[tableOffset3 >> 7]) >> 2; waveValue4 = (waveTable[tableOffset4 >> 7]) >> 1; summedValue = waveValue1 + waveValue2 + waveValue3 + waveValue4; PORTD = (((signed int)(summedValue * envelopeValue)) >> 8) + 0x80; if(noteTime==0) { preScale = 20; noteState = 3; } else { noteTime = noteTime - 1; } break; case 3: // decay tableOffset1 = tableOffset1 + tableStep1; tableOffset2 = tableOffset2 + tableStep2; tableOffset3 = tableOffset3 + tableStep3; tableOffset4 = tableOffset4 + tableStep4; waveValue1 = (waveTable[tableOffset1 >> 7]) >> 4; waveValue2 = (waveTable[tableOffset2 >> 7]) >> 4; waveValue3 = (waveTable[tableOffset3 >> 7]) >> 2; waveValue4 = (waveTable[tableOffset4 >> 7]) >> 1; summedValue = waveValue1 + waveValue2 + waveValue3 + waveValue4; PORTD = (((signed int)(summedValue * envelopeValue)) >> 8) + 0x80; preScale--; if(preScale==0) { preScale = 80; if(envelopeValue != 0) { envelopeValue--; } else { tableOffset1 = 0; tableOffset2 = 0; tableOffset3 = 0; tableOffset4 = 0; tableStep1 = 0; tableStep2 = 0; tableStep3 = 0; tableStep4 = 0; // noteTime = 0xc35; // noteTime = 0x2000; if (tuneNotes[i]==0) i=0; noteState = 0; } } break; } PORTB &= 0xFE; } void loop() { }
The notes for the tune are held in the tuneNotes[] array. There are four numbers for the partials and a number for note duration (the note duration part of it isn't very well worked out - this is an experiment rather than a project). Above the array I've included, as comments, a whole scale of notes - that makes it easy to assemble a tune just by copying and pasteing.
Demonstration
Here it is playing a familiar tune.
It's not too bad - the notes do have a metallic, tinkly quality to them, but they don't really resonate in quite the way that a bell or chime would and they feel like the beginning has been removed. With a real chime or bell there's a period of noise at the start, when the energy arrives from the hammer/clanger and before the resonances of the structure filter it to particular frequencies, which I'm not modelling. It's also the case that each partial would decay at different rates and go on for longer than mine do. That's all too much for a simple Arduino to handle - one for you people with more impressive and capable SBCs, I think.
[1] Fundamentals of Musical Acoustics. Arthur H. Benade.
Top Comments