RTTTL
In this section we add the capability to play songs from cellphone ringtones
We had a functioning theremin by the end of the previous tutorial, but only one song transcribed. I am not a musician, and it is beyond my skill to transcribe songs, I ended up getting someone else to do the transcription for the last tutorial for me. I really wanted find a way to play songs that came in electronic format already, and not bother with transcribing. I was able to to do this by incorporating cell phone ringtones using RTTTL (Ring Tone Text Transfer Language). In other words it’s the sheet music for cell phone ring tones in a format that we can use, and you can find a rather stupendous number of songs online here. This is not the format that was used to transcribe the first song, at first I thought of just using the RTTTL format for all songs, but this wasn’t quite right because RTTTL does not have a volume specification, so I decided to write a routine to convert the RTTTL data to our custom Song data type. Then we can experiment with adding in some volume control.
There is an example sketch in Tone library that has most of what we need for this conversion. To load the example, open the Arduino IDE and Click
File->Examples->Tone->RTTTL.
There is nothing particularly complicated about the RTTTL2Song() function it just takes a bit of work to do all the necessary conversion. You need to understand a little bit about the RTTTL specification. We specify a song as a string.
char *RTTLSongs[] = {"Indiana Jones:d=4,o=5,b=250:e,8p,8f,8g,8p,1c6,8p.,d,8p,8e,1f,p.,g,8p,8a,8b,8p,1f6,p,a,8p,8b,2c6,2d6,2e6,e,8p,8f,8g,8p,1c6,p,d6,8p,8e6,1f.6,g,8p,8g,e.6,8p,d6,8p,8g,e.6,8p,d6,8p,8g,f.6,8p,e6,8p,8d6,2c6"};
This is actually an array of strings with just one entry, we’ll add more in minute. The format is
song name: default values: song data:
For the above example, the song name is “Indiana Jones”, the default duration is 4, which means 1/4 note, the default octave is 5, and the default tempo is 250 beats per minute. In the data section, notes will be specified as duration,octave,note,dot. If the given data is not there, then the default is used. For example 8e6 would mean play note e6 for a duration of 8. If the entry was simply e, then this would play note e5 ( because 5 is the default octave) for the default duration of 4. If a dot appears as in 1f.6, this would mean play note f6 for a duration of 1.5. The dot means increase the duration by half. Here is the code that does that
// now, get optional '.' dotted note if(*p == '.') { duration += duration/2; p++; }
So far I have just been talking about a note duration as some integer, how does that translate into seconds? The duration calculation is done with the following two snippets of code, bpm is the beats per minute, and is set in the RTTTL2Song() function.
wholenote = (60 * 1000L / bpm) * 4; // this is the time for whole note (in milliseconds)
// first, get note duration, if available num = 0; while(isdigit(*p)) { num = (num * 10) + (*p++ - '0'); } if(num) duration = wholenote / num; else duration = wholenote / default_dur; // we will need to check if we are a dotted note after
The line
num = (num * 10) + (*p++ - '0');
converts a multiple digit string into the correct number, e.g. the chars ‘4’ and ‘3’ becomes the integer 43. Then if we have read an actual number, the duration is equal to wholenote divided by the number we read. For example if bpm was set to 60, and we had 8e6, the 8 (i.e. 1/8th note ) would get read, and we would have
(60 * 1000) / 60 * 4 = 4000 / 8 = 500 milliseconds.
If we don’t read a digit, then the calculation above is done with the default duration.
If you are new to programming or C programming in particular, the way the data is being passed to and from the RTTTL2Song() function might be a little bit confusing in that we are using pass by reference, and pointers to dynamically allocated user defined structs, these are very confusing topics for someone learning the C programming language for the first time. I can remember the first time I learned about it, it took me a long time to get my head wrapped around the concept, and it still confuses the crap out of me at times. More on this in a minute. Let’s look at how the songs array get populated. First we want to load up all our song strings into the header file.
char *RTTLSongs[] = {"Indiana Jones:d=4,o=5,b=250:e,8p,8f,8g,8p,1c6,8p.,d,8p,8e,1f,p.,g,8p,8a,8b,8p,1f6,p,a,8p,8b,2c6,2d6,2e6,e,8p,8f,8g,8p,1c6,p,d6,8p,8e6,1f.6,g,8p,8g,e.6,8p,d6,8p,8g,e.6,8p,d6,8p,8g,f.6,8p,e6,8p,8d6,2c6", "TheSimpsons:d=4,o=5,b=160:c.6,e6,f#6,8a6,g.6,e6,c6,8a,8f#,8f#,8f#,2g,8p,8p,8f#,8f#,8f#,8g,a#.,8c6,8c6,8c6,c6", "TakeOnMe:d=4,o=4,b=160:8f#5,8f#5,8f#5,8d5,8p,8b,8p,8e5,8p,8e5,8p,8e5,8g#5,8g#5,8a5,8b5,8a5,8a5,8a5,8e5,8p,8d5,8p,8f#5,8p,8f#5,8p,8f#5,8e5,8e5,8f#5,8e5,8f#5,8f#5,8f#5,8d5,8p,8b,8p,8e5,8p,8e5,8p,8e5,8g#5,8g#5,8a5,8b5,8a5,8a5,8a5,8e5,8p,8d5,8p,8f#5,8p,8f#5,8p,8f#5,8e5,8e5", "Entertainer:d=4,o=5,b=140:8d,8d#,8e,c6,8e,c6,8e,2c.6,8c6,8d6,8d#6,8e6,8c6,8d6,e6,8b,d6,2c6,p,8d,8d#,8e,c6,8e,c6,8e,2c.6,8p,8a,8g,8f#,8a,8c6,e6,8d6,8c6,8a,2d6", "Muppets:d=4,o=5,b=250:c6,c6,a,b,8a,b,g,p,c6,c6,a,8b,8a,8p,g.,p,e,e,g,f,8e,f,8c6,8c,8d,e,8e,8e,8p,8e,g,2p,c6,c6,a,b,8a,b,g,p,c6,c6,a,8b,a,g.,p,e,e,g,f,8e,f,8c6,8c,8d,e,8e,d,8d,c",
Next we declare variables for numSongs and numRTTLSongs
int numRTTLSongs = sizeof(RTTLSongs) / sizeof(RTTLSongs[0]); int numSongs = numRTTLSongs + 1;
Then we declare the Songs[] array, this is done by declaring a pointer to a Song and allocating enough memory to hold the number of songs we have declared. It is best to do it this way, so we don’t have to bother with changing the size of the Songs[] array every time we add or remove a song from the header file.
Song *Songs = (Song*) calloc( numSongs, sizeof(Song));
We also need to declare the RTTTL2Song() function
// convert RTTL string into Song void RTTL2Song( char* RTTL, Song *newSong);
The first parameter is our song represented as a string, the second parameter is a pointer to a Song.
Next we add our non RTTL type songs to the songs array
// load songs array with non-RTTTL songs
Songs = SomewhereOTR;
Songs[0] = *SomewhereOTR;
Finally to load up all the RTTL Songs we use a for loop
// load songs array with RTTTL songs for( int i = 0; i < numSongs; i++) { RTTL2Song(RTTLSongs[i], &Songs[i+1]); }
Notice that we are using the & operator when passing Songs[i+1], this means we are passing the address of the (i+1)th element of the Songs array. This is how pass by reference works in C programming. You pass an address, then your function operates on the actual data and not a copy of it. Like I was saying earlier, this is a pretty confusing topic for beginners. A more thorough explanation can be found in any introductory book on C programming. Also There are many good explanations online, like here.
That’s about all I think I’ll cover for this project, if you have made it this far, that’s awesome, this project turned out to be a lot more involved than I thought it would be when I first started it. If not then I at least hope you learned something new. I have a few ideas on how this project could be continued, first of course is to make it a hands free instrument. This would be difficult to do with a radio frequency circuit like a true theremin, but it might be possible using ultrasonic sensors. Another idea that I’d like to do someday is to add some granular synthesis, this way the servo that controls the frequency would control a single grain, then another potentiometer would be added to control the 2nd grain. You would be able to change the character of the instrument with this second potentiometer. One last idea would be to add a third push button that alternates between a square, triangle and sin wave, similar to the simple waveform generator example from the Arduino site.
Finally here are some more videos playing various songs, followed by the complete code to build this instrument.
The Simpsons
Mission Impossible
Jeopardy
James Bond
Inspector Gadget
Imperial March
Entertainer
Airwolf
And here is the complete sketch.
songdata.h
#include <string> #define NOTE_Cm1 0 #define NOTE_CSm1 1 #define NOTE_Dm1 2 #define NOTE_DSm1 3 #define NOTE_Em1 4 #define NOTE_Fm1 5 #define NOTE_FSm1 6 #define NOTE_Gm1 7 #define NOTE_GSm1 8 #define NOTE_Am1 9 #define NOTE_ASm1 10 #define NOTE_Bm1 11 #define NOTE_C0 12 #define NOTE_CS0 13 #define NOTE_D0 14 #define NOTE_DS0 15 #define NOTE_E0 16 #define NOTE_F0 17 #define NOTE_FS0 18 #define NOTE_G0 19 #define NOTE_GS0 20 #define NOTE_A0 21 #define NOTE_AS0 22 #define NOTE_B0 23 #define NOTE_C1 24 #define NOTE_CS1 25 #define NOTE_D1 26 #define NOTE_DS1 27 #define NOTE_E1 28 #define NOTE_F1 29 #define NOTE_FS1 30 #define NOTE_G1 31 #define NOTE_GS1 32 #define NOTE_A1 33 #define NOTE_AS1 34 #define NOTE_B1 35 #define NOTE_C2 36 #define NOTE_CS2 37 #define NOTE_D2 38 #define NOTE_DS2 39 #define NOTE_E2 40 #define NOTE_F2 41 #define NOTE_FS2 42 #define NOTE_G2 43 #define NOTE_GS2 44 #define NOTE_A2 45 #define NOTE_AS2 46 #define NOTE_B2 47 #define NOTE_C3 48 #define NOTE_CS3 49 #define NOTE_D3 50 #define NOTE_DS3 51 #define NOTE_E3 52 #define NOTE_F3 53 #define NOTE_FS3 54 #define NOTE_G3 55 #define NOTE_GS3 56 #define NOTE_A3 57 #define NOTE_AS3 58 #define NOTE_B3 59 #define NOTE_C4 60 #define NOTE_CS4 61 #define NOTE_D4 62 #define NOTE_DS4 63 #define NOTE_E4 64 #define NOTE_F4 65 #define NOTE_FS4 66 #define NOTE_G4 67 #define NOTE_GS4 68 #define NOTE_A4 69 #define NOTE_AS4 70 #define NOTE_B4 71 #define NOTE_C5 72 #define NOTE_CS5 73 #define NOTE_D5 74 #define NOTE_DS5 75 #define NOTE_E5 76 #define NOTE_F5 77 #define NOTE_FS5 78 #define NOTE_G5 79 #define NOTE_GS5 80 #define NOTE_A5 81 #define NOTE_AS5 82 #define NOTE_B5 83 #define NOTE_C6 84 #define NOTE_CS6 85 #define NOTE_D6 86 #define NOTE_DS6 87 #define NOTE_E6 88 #define NOTE_F6 89 #define NOTE_FS6 90 #define NOTE_G6 91 #define NOTE_GS6 92 #define NOTE_A6 93 #define NOTE_AS6 94 #define NOTE_B6 95 #define NOTE_C7 96 #define NOTE_CS7 97 #define NOTE_D7 98 #define NOTE_DS7 99 #define NOTE_E7 100 #define NOTE_F7 101 #define NOTE_FS7 102 #define NOTE_G7 103 #define NOTE_GS7 104 #define NOTE_A7 105 #define NOTE_AS7 106 #define NOTE_B7 107 #define NOTE_C8 108 #define NOTE_CS8 109 #define NOTE_D8 110 #define NOTE_DS8 111 #define NOTE_E8 112 #define NOTE_F8 113 #define NOTE_FS8 114 #define NOTE_G8 115 #define NOTE_GS8 116 #define NOTE_A8 117 #define NOTE_AS8 118 #define NOTE_B8 119 #define NOTE_C9 120 #define NOTE_CS9 121 #define NOTE_D9 122 #define NOTE_DS9 123 #define NOTE_E9 124 #define NOTE_F9 125 #define NOTE_FS9 126 #define NOTE_G9 127 //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Song Data //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #define MAX_SONG_SIZE 200 struct songData { int note; int duration; int volume; }; struct Song { int SongLength; char *name; songData *data; Song( char* songName, songData *newSongData, int songLength); Song(); }; // somewhere over the rainbow songData SWOTR[] = {{NOTE_GS3, 67, 8}, {NOTE_GS4, 74, 8}, {NOTE_G4, 40, 9}, {NOTE_DS4, 16, 8}, {NOTE_F4, 20, 9}, {NOTE_G4, 38, 8}, {NOTE_GS4, 47, 7}, {NOTE_GS3, 37, 8}, {NOTE_F4, 75, 8}, {NOTE_DS4, 130, 6}, {NOTE_F3, 77, 8}, {NOTE_CS4, 73, 6}, {NOTE_C4, 41, 8}, {NOTE_GS3, 16, 7}, {NOTE_AS3, 19, 9}, {NOTE_C4, 37, 9}, {NOTE_CS4, 39, 6}, {NOTE_AS3, 42, 8}, {NOTE_G3, 9, 9}, {NOTE_GS3, 13, 7}, {NOTE_AS3, 23, 8}, {NOTE_C4, 27, 7}, {NOTE_GS3, 114, 7}, {NOTE_GS3, 72, 8}, {NOTE_GS4, 77, 6}, {NOTE_G4, 41, 7}, {NOTE_DS4, 19, 8}, {NOTE_F4, 20, 9}, {NOTE_G4, 42, 9}, {NOTE_GS4, 53, 7}, {NOTE_GS3, 43, 9}, {NOTE_F4, 79, 8}, {NOTE_DS4, 128, 8}, {NOTE_F3, 38, 9}, {NOTE_CS4, 78, 7}, {NOTE_C4, 47, 8}, {NOTE_GS3, 16, 8}, {NOTE_AS3, 18, 8}, {NOTE_C4, 58, 7}, {NOTE_CS4, 40, 7}, {NOTE_AS3, 43, 9}, {NOTE_G3, 13, 9}, {NOTE_GS3, 13, 8}, {NOTE_AS3, 51, 8}, {NOTE_C4, 63, 8}, {NOTE_GS3, 93, 5}, {NOTE_DS4, 97, 8}, {NOTE_C4, 32, 6}, {NOTE_DS4, 23, 5}, {NOTE_C4, 19, 8}, {NOTE_DS4, 20, 7}, {NOTE_C4, 17, 8}, {NOTE_DS4, 19, 8}, {NOTE_C4, 20, 8}, {NOTE_DS4, 20, 9}, {NOTE_CS4, 18, 7}, {NOTE_DS4, 17, 8}, {NOTE_CS4, 16, 7}, {NOTE_DS4, 18, 8}, {NOTE_CS4, 16, 7}, {NOTE_DS4, 19, 8}, {NOTE_CS4, 21, 8}, {NOTE_DS4, 26, 8}, {NOTE_F4, 81, 9}, {NOTE_F4, 148, 8}, {NOTE_DS4, 19, 7}, {NOTE_C4, 16, 7}, {NOTE_DS4, 17, 7}, {NOTE_C4, 15, 8}, {NOTE_DS4, 17, 7}, {NOTE_C4, 15, 9}, {NOTE_DS4, 17, 8}, {NOTE_C4, 19, 8}, {NOTE_D4, 20, 8}, {NOTE_D4, 17, 9}, {NOTE_F4, 16, 9}, {NOTE_D4, 15, 9}, {NOTE_F4, 15, 9}, {NOTE_D4, 16, 8}, {NOTE_F4, 18, 9}, {NOTE_D4, 21, 9}, {NOTE_F4, 23, 9}, {NOTE_G4, 82, 10}, {NOTE_G4, 94, 8}, {NOTE_AS4, 108, 9}, {NOTE_F4, 161, 6}, {NOTE_GS3, 75, 8}, {NOTE_GS4, 74, 8}, {NOTE_G4, 38, 9}, {NOTE_DS4, 20, 8}, {NOTE_F4, 19, 9}, {NOTE_G4, 41, 8}, {NOTE_GS4, 53, 8}, {NOTE_GS3, 44, 9}, {NOTE_F4, 74, 8}, {NOTE_DS4, 119, 7}, {NOTE_F3, 36, 8}, {NOTE_CS4, 76, 9}, {NOTE_C4, 43, 8}, {NOTE_GS3, 19, 8}, {NOTE_AS3, 21, 9}, {NOTE_C4, 49, 8}, {NOTE_CS4, 61, 7}, {NOTE_AS3, 56, 8}, {NOTE_G3, 53, 7}, {NOTE_G3, 31, 8}, {NOTE_AS3, 66, 8}, {NOTE_C4, 98, 8}, {NOTE_GS3, 139, 8}, {NOTE_DS4, 22, 8}, {NOTE_C4, 18, 7}, {NOTE_DS4, 20, 7}, {NOTE_C4, 17, 7}, {NOTE_DS4, 20, 8}, {NOTE_C4, 19, 8}, {NOTE_DS4, 23, 7}, {NOTE_C4, 30, 6}, {NOTE_DS4, 24, 7}, {NOTE_CS4, 23, 7}, {NOTE_DS4, 22, 6}, {NOTE_CS4, 26, 7}, {NOTE_DS4, 38, 7}, {NOTE_CS4, 51, 7}, {NOTE_DS4, 80, 8}, {NOTE_F4, 101, 9}, {NOTE_G4, 130, 9}, {NOTE_GS4, 261, 8}}; char *RTTLSongs[] = {"Indiana Jones:d=4,o=5,b=250:e,8p,8f,8g,8p,1c6,8p.,d,8p,8e,1f,p.,g,8p,8a,8b,8p,1f6,p,a,8p,8b,2c6,2d6,2e6,e,8p,8f,8g,8p,1c6,p,d6,8p,8e6,1f.6,g,8p,8g,e.6,8p,d6,8p,8g,e.6,8p,d6,8p,8g,f.6,8p,e6,8p,8d6,2c6", "TheSimpsons:d=4,o=5,b=160:c.6,e6,f#6,8a6,g.6,e6,c6,8a,8f#,8f#,8f#,2g,8p,8p,8f#,8f#,8f#,8g,a#.,8c6,8c6,8c6,c6", "Entertainer:d=4,o=5,b=140:8d,8d#,8e,c6,8e,c6,8e,2c.6,8c6,8d6,8d#6,8e6,8c6,8d6,e6,8b,d6,2c6,p,8d,8d#,8e,c6,8e,c6,8e,2c.6,8p,8a,8g,8f#,8a,8c6,e6,8d6,8c6,8a,2d6", "Muppets:d=4,o=5,b=250:c6,c6,a,b,8a,b,g,p,c6,c6,a,8b,8a,8p,g.,p,e,e,g,f,8e,f,8c6,8c,8d,e,8e,8e,8p,8e,g,2p,c6,c6,a,b,8a,b,g,p,c6,c6,a,8b,a,g.,p,e,e,g,f,8e,f,8c6,8c,8d,e,8e,d,8d,c", "Bond:d=4,o=5,b=80:32p,16c#6,32d#6,32d#6,16d#6,8d#6,16c#6,16c#6,16c#6,16c#6,32e6,32e6,16e6,8e6,16d#6,16d#6,16d#6,16c#6,32d#6,32d#6,16d#6,8d#6,16c#6,16c#6,16c#6,16c#6,32e6,32e6,16e6,8e6,16d#6,16d6,16c#6,16c#7,c.7,16g#6,16f#6,g#.6", "StarWars:d=4,o=5,b=45:32p,32f#,32f#,32f#,8b.,8f#.6,32e6,32d#6,32c#6,8b.6,16f#.6,32e6,32d#6,32c#6,8b.6,16f#.6,32e6,32d#6,32e6,8c#.6,32f#,32f#,32f#,8b.,8f#.6,32e6,32d#6,32c#6,8b.6,16f#.6,32e6,32d#6,32c#6,8b.6,16f#.6,32e6,32d#6,32e6,8c#6", "GoodBad:d=4,o=5,b=56:32p,32a#,32d#6,32a#,32d#6,8a#.,16f#.,16g#.,d#,32a#,32d#6,32a#,32d#6,8a#.,16f#.,16g#.,c#6,32a#,32d#6,32a#,32d#6,8a#.,16f#.,32f.,32d#.,c#,32a#,32d#6,32a#,32d#6,8a#.,16g#.,d#", "A-Team:d=8,o=5,b=120:4d#6,a#,2d#6,16p,g#,4a#,4d#.,p,16g,16a#,d#6,a#,f6,2d#6,16p,c#.6,16c6,16a#,g#.,2a#,d#6,a#,2d#6,16p,g#,4a#,4d#.,p,16g,16a#,d#6,a#,f6,2d#6,16p,c#.6,16c6,16a#,g#.,2a#", "Flinstones:d=4,o=5,b=40:32p,16f6,16a#,16a#6,32g6,16f6,16a#.,16f6,32d#6,32d6,32d6,32d#6,32f6,16a#,16c6,d6,16f6,16a#.,16a#6,32g6,16f6,16a#.,32f6,32f6,32d#6,32d6,32d6,32d#6,32f6,16a#,16c6,a#,16a6,16d.6,16a#6,32a6,32a6,32g6,32f#6,32a6,8g6,16g6,16c.6,32a6,32a6,32g6,32g6,32f6,32e6,32g6,8f6,16f6,16a#.,16a#6,32g6,16f6,16a#.,16f6,32d#6,32d6,32d6,32d#6,32f6,16a#,16c.6,32d6,32d#6,32f6,16a#,16c.6,32d6,32d#6,32f6,16a#6,16c7,8a#.6", "Jeopardy:d=4,o=6,b=125:c,f,c,f5,c,f,2c,c,f,c,f,a.,8g,8f,8e,8d,8c#,c,f,c,f5,c,f,2c,f.,8d,c,a#5,a5,g5,f5,p,d#,g#,d#,g#5,d#,g#,2d#,d#,g#,d#,g#,c.7,8a#,8g#,8g,8f,8e,d#,g#,d#,g#5,d#,g#,2d#,g#.,8f,d#,c#,c,p,a#5,p,g#.5,d#,g#", "Gadget:d=16,o=5,b=40:32d#,32f,32f#,32g#,a#,f#,a,f,g#,f#,32d#,32f,32f#,32g#,a#,d#6,4d6,32d#,32f,32f#,32g#,a#,f#,a,f,g#,f#,8d#", "MissionImp:d=16,o=6,b=75:32d,32d#,32d,32d#,32d,32d#,32d,32d#,32d,32d,32d#,32e,32f,32f#,32g,g,8p,g,8p,a#,p,c7,p,g,8p,g,8p,f,p,f#,p,g,8p,g,8p,a#,p,c7,p,g,8p,g,8p,f,p,f#,p,a#,g,2d,32p,a#,g,2c#,32p,a#,g,2c,a#5,8c,2p,32p,a#5,g5,2f#,32p,a#5,g5,2f,32p,a#5,g5,2e,d#,8d", "Airwolf:d=4,o=5,b=100:e,16a,16b,16d6,e6,16g6,16f#6,16d6,e6,16g6,16f#6,16d6,e6,8d6,16f#6,b,a,8g,16a,8f#,16d,g,16c6,16d6,16f6,g,16c6,16b6,16f6,g6,16c6,16b6,16f6,g6,8f6,16a,d6,c6,8b,16d6,8a,16f,g6,16c6,16d6,16f6,g6,16c6,16b6,16f6", "StarWars:d=4,o=5,b=112:8d.,16p,8d.,16p,8d.,16p,8a#4,16p,16f,8d.,16p,8a#4,16p,16f,d.,8p,8a.,16p,8a.,16p,8a.,16p,8a#,16p,16f,8c#.,16p,8a#4,16p,16f,d.,8p,8d.6,16p,8d,16p,16d,8d6,8p,8c#6,16p,16c6,16b,16a#,8b,8p,16d#,16p,8g#,8p,8g,16p,16f#,16f,16e,8f,8p,16a#4,16p,8c#,8p,8a#4,16p,16c#,8f.,16p,8d,16p,16f,a.,8p,8d.6,16p,8d,16p,16d,8d6,8p,8c#6,16p,16c6,16b,16a#,8b,8p,16d#,16p,8g#,8p,8g,16p,16f#,16f,16e,8f,8p,16a#4,16p,8c#"};
and here is DDS_theremin.ino
// this code reuses code from several places, for the DDS part, look here // // http://rcarduino.blogspot.com/2012/12/arduino-due-dds-part-1-sinewaves-and.html // http://interface.khm.de/index.php/lab/experiments/arduino-dds-sinewave-generator/ // // for timer interrupts see // http://arduino.cc/forum/index.php?action=post;topic=130423.15;num_replies=20 // // for controlling the lcd, most was taken from the examples included with the Arduino install // File->Examples->LiquidCrystal->HelloWorld // // These are the clock frequencies available to the timers /2,/8,/32,/128 // 84Mhz/2 = 42.000 MHz // 84Mhz/8 = 10.500 MHz // 84Mhz/32 = 2.625 MHz // 84Mhz/128 = 656.250 KHz // // 44.1Khz = CD Sample Rate // Lets aim for as close to the CD Sample Rate as we can get - // // 42Mhz/44.1Khz = 952.38 // 10.5Mhz/44.1Khz = 238.09 // best fit divide by 8 = TIMER_CLOCK2 and 238 ticks per sample // 2.625Hmz/44.1Khz = 59.5 // 656Khz/44.1Khz = 14.88 // use for servo control 13120 / 65600 = .02 ( update servos every 20 milliseconds ) // 84Mhz/44.1Khz = 1904 instructions per tick // include the library code: #include <LiquidCrystal.h> #include <Servo.h> #include "songdata.h" #include <string> //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // INPUT PARAMS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #define btnNext 0 #define btnSelect 1 #define btnNextPin 50 #define btnSelectPin 51 #define freqServoPin 9 #define volServoPin 8 int fMinInput = 214; // corresponds to 26 degrees int fMaxInput = 594; // corresponds approx to 154 degrees int fMinAngle = 26; int fMaxAngle = 154; int minNote = 50; int maxNote = 100; int vMinInput = 350; int vMaxInput = 435; int vMinAngle = 75; int vMaxAngle = 105; int scaleFactor = (fMaxInput - fMinInput) / (maxNote - minNote); float fAngle = 0.0; float vAngle = 0.0; Servo freqServo; Servo volServo; int lastInterrupt = 0; // time since last interrupt, for switch debouncing void BtnNext(); void BtnSelect(); // we are using a servo as a potentiometer to control the volume. #define MAX_VOL 10 // we'll have 10 volume levels that can be set in software uint32_t volume = MAX_VOL; // var we'll actually use to control the volume //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // DDS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // the phase accumulator points to the current sample in our wavetable uint32_t ulPhaseAccumulator = 0; // the phase increment controls the rate at which we move through the wave table // higher values = higher frequencies volatile uint32_t ulPhaseIncrement = 0; // 32 bit phase increment, see below // full waveform = 0 to SAMPLES_PER_CYCLE // Phase Increment for 1 Hz =(SAMPLES_PER_CYCLE_FIXEDPOINT/SAMPLE_RATE) = 1Hz // Phase Increment for frequency F = (SAMPLES_PER_CYCLE/SAMPLE_RATE)*F #define SAMPLE_RATE 44100.0 #define SAMPLES_PER_CYCLE 600 #define SAMPLES_PER_CYCLE_FIXEDPOINT (SAMPLES_PER_CYCLE<<20) #define TICKS_PER_CYCLE (float)((float)SAMPLES_PER_CYCLE_FIXEDPOINT/(float)SAMPLE_RATE) // We have 521K flash and 96K ram to play with // Create a table to hold the phase increments we need to generate midi note frequencies at our 44.1Khz sample rate #define MIDI_NOTES 128 uint32_t nMidiPhaseIncrement[MIDI_NOTES]; // Create a table to hold pre computed sinewave, the table has a resolution of 600 samples #define WAVE_SAMPLES 600 // default int is 32 bit, in most cases its best to use uint32_t but for large arrays its better to use smaller // data types if possible, here we are storing 12 bit samples in 16 bit ints uint16_t nSineTable[WAVE_SAMPLES]; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Song Data //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void displayLCD( char* line1, char* line2); // initialize the library with the numbers of the interface pins LiquidCrystal lcd(12, 11, 5, 4, 3, 2); int playingSong = false; // state variable to indicate a preloaded song should be playing int iNote = 0; // index to indicate what note of particular song is playing uint32_t interruptCtr = 0; // ctr used for timing servo PWM uint32_t volInterruptCtr = 0; // ctr used for timing servo PWM uint32_t noteDurationCtr = 0; // ctr used to control how long a given song note should play Song::Song( char *songName, songData *newSongData, int songLength) { this->name = songName; this->data = newSongData; this->SongLength = songLength; }; // declare all non-RTTL songs here Song *SomewhereOTR = new Song::Song("SWOTR", SWOTR, sizeof(SWOTR)/sizeof(SWOTR[0])); // declare vars for the number of songs int numRTTLSongs = sizeof(RTTLSongs) / sizeof(RTTLSongs[0]); int numSongs = numRTTLSongs + 1; Song *Songs = (Song*) calloc( numSongs, sizeof(Song)); int iCurrSong = 0; // function called to play a demo void PlaySong( songData* song ); // convert RTTL string into Song void RTTL2Song( char* RTTL, Song *newSong); void setup() { Serial.begin(9600); // set up the LCD's number of columns and rows: lcd.begin(16, 2); // load songs array with non-RTTTL songs Songs = SomewhereOTR; // load songs array with RTTTL songs for( int i = 0; i < numSongs; i++) { RTTL2Song(RTTLSongs[i], &Songs[i+1]); } // Print a message to the LCD. displayLCD("Press Sel 4Demo", Songs[0].name ); pinMode(btnNextPin, INPUT); pinMode(btnSelectPin, INPUT); attachInterrupt(btnNextPin, BtnNext, CHANGE); attachInterrupt(btnSelectPin, BtnSelect, CHANGE); createNoteTable(SAMPLE_RATE); createSineTable(); /* turn on the timer clock in the power management controller */ pmc_set_writeprotect(false); // disable write protection for pmc registers pmc_enable_periph_clk(ID_TC6); // enable peripheral clock TC6 pmc_enable_periph_clk(ID_TC7); // enable peripheral clock TC7 /* we want wavesel 01 with RC */ TC_Configure(/* clock */TC2,/* channel */0, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC | TC_CMR_TCCLKS_TIMER_CLOCK2); TC_Configure(/* clock */TC2,/* channel */1, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC | TC_CMR_TCCLKS_TIMER_CLOCK4); TC_SetRC(TC2, 0, 238); // sets <> 44.1 Khz interrupt rate TC_SetRC(TC2, 1, 13120); TC_Start(TC2, 0); TC_Start(TC2, 1); // enable timer interrupts on the timer TC2->TC_CHANNEL[0].TC_IER=TC_IER_CPCS; // IER = interrupt enable register TC2->TC_CHANNEL[0].TC_IDR=~TC_IER_CPCS; // IDR = interrupt disable register TC2->TC_CHANNEL[1].TC_IER=TC_IER_CPCS; // IER = interrupt enable register TC2->TC_CHANNEL[1].TC_IDR=~TC_IER_CPCS; // IDR = interrupt disable register /* Enable the interrupt in the nested vector interrupt controller */ /* TC4_IRQn where 4 is the timer number * timer channels (3) + the channel number (=(1*3)+1) for timer1 channel1 */ NVIC_EnableIRQ(TC6_IRQn); NVIC_EnableIRQ(TC7_IRQn); // this is a cheat - enable the DAC analogWrite(DAC1,0); } void loop() { // read analog input 0, drop the range from 0-1024 to minNote-maxNote // then look up the phaseIncrement required to generate the note in our nMidiPhaseIncrement table uint32_t ulInput = min( max( analogRead(0), fMinInput ), fMaxInput); ulInput = ((ulInput - fMinInput) / scaleFactor) + minNote; // scale to minNote to maxNote // get volume from analog input, then scale it to range vMinInput to vMaxInput volume = min( max( analogRead(1), vMinInput ), vMaxInput); volume = ((volume - vMinInput) / 8); ulPhaseIncrement = nMidiPhaseIncrement[ulInput]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // INTERRUPT HANDLERS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // this is the interrupt handler that is called at our sample rate of 44100 times per second, its primary job is to update // the value writtento the DAC port void TC6_Handler() { // We need to get the status to clear it and allow the interrupt to fire again TC_GetStatus(TC2, 0); ulPhaseAccumulator += ulPhaseIncrement; // 32 bit phase increment, see below // if the phase accumulator over flows - we have been through one cycle at the current pitch, // now we need to reset the grains ready for our next cycle if(ulPhaseAccumulator > SAMPLES_PER_CYCLE_FIXEDPOINT) { // DB 02/Jan/2012 - carry the remainder of the phase accumulator ulPhaseAccumulator -= SAMPLES_PER_CYCLE_FIXEDPOINT; } // get the current sample uint32_t ulOutput = volume * nSineTable[ulPhaseAccumulator>>20]; // we cheated and user analogWrite to enable the dac, but here we want to be fast so // write directly dacc_write_conversion_data(DACC_INTERFACE, ulOutput); } // this is the interrupt handler that is called every 20 mS (i.e. time between pulses sent to a servo), its job is to play a demo of a song void TC7_Handler() { // We need to get the status to clear it and allow the interrupt to fire again TC_GetStatus(TC2, 1); // handle servo output if we are in song playing mode if( playingSong ) { fAngle = ((float)(Songs[iCurrSong].data[iNote].note - minNote) / (float)(maxNote - minNote)) * (fMaxAngle - fMinAngle) + fMinAngle; freqServo.write(fAngle); // test if we need to increment to the next note if( noteDurationCtr >= Songs[iCurrSong].data[iNote].duration ) { noteDurationCtr = 0; // increment note and test if we have finished the song if( ++iNote >= Songs[iCurrSong].SongLength ) { // song is over playingSong = false; iNote = 0; freqServo.detach(); volServo.detach(); displayLCD("Press sel 4Demo", Songs[iCurrSong].name ); } else { // set volume for next note volume = Songs[iCurrSong].data[iNote].volume; vAngle = vMinAngle + volume / (float) MAX_VOL *(float)(vMaxAngle - vMinAngle); volServo.write(vAngle); } } noteDurationCtr++; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // SONG RELATED FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // create the notes we want to be abale to play, i.e. a list of frequencies void createNoteTable(float fSampleRate) { for(uint32_t unMidiNote = 0; unMidiNote < 128; unMidiNote++) { float fFrequency = ((pow(2.0,(unMidiNote-69.0)/12.0)) * 440.0); nMidiPhaseIncrement[unMidiNote] = fFrequency*TICKS_PER_CYCLE; } } // create the individual samples for our sinewave table void createSineTable() { for(uint32_t nIndex = 0;nIndex < WAVE_SAMPLES;nIndex++) { // normalised to 12 bit range 0-4095 nSineTable[nIndex] = (uint32_t) (((1+sin(((2.0*PI)/WAVE_SAMPLES)*nIndex))*4095.0)/2) / MAX_VOL; } } // function to call when a song is to be played void PlaySong(struct songData* song) { displayLCD("Playing", Songs[iCurrSong].name); playingSong = true; // enable servos for output freqServo.attach(9); volServo.attach(8); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // I/O FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // routine to display text to the LCD void displayLCD( char* line1, char* line2) { lcd.clear(); lcd.setCursor(0,0); lcd.print(line1); lcd.setCursor(0,1); lcd.print(line2); } // interrupt handler for next button void BtnNext() { if( millis() - lastInterrupt < 500 ) { return; } else { lastInterrupt = millis(); // increment song num iCurrSong = ((iCurrSong+1) % numSongs); displayLCD("Press sel 4Demo", Songs[iCurrSong].name ); } } // interrupt handler for select button void BtnSelect() { if( millis() - lastInterrupt < 500 ) { return; } else { lastInterrupt = millis(); // Play Current song PlaySong(Songs[iCurrSong].data); } } // populates newSong with dta from RTTL void RTTL2Song(char* RTTL, Song *newSong) { char* p = RTTL; int num=0; int default_dur = 4; int default_oct = 4; int bpm = 63; int wholenote; int duration = 0; int note; int iNote = 0; int scale; #define OCTAVE_OFFSET 0 char *pstart = p; int numChars = 0; while(*p != ':') { p++; numChars++;} newSong->name = (char*)calloc(numChars, sizeof(char)); strncpy((*newSong).name, pstart, numChars); // copy name to newSong p++; // skip ':' // get default duration if(*p == 'd') { p++; p++; // skip "d=" num = 0; while(isdigit(*p)) num = (num * 10) + (*p++ - '0'); if(num > 0) default_dur = num; p++; // skip comma // get default octave if(*p == 'o') { p++; p++; // skip "o=" num = *p++ - '0'; if(num >= 3 && num <=7) default_oct = num; p++; // skip comma } // get BPM if(*p == 'b') { p++; p++; // skip "b=" num = 0; while(isdigit(*p)) { num = (num * 10) + (*p++ - '0'); } bpm = num; p++; // skip colon } wholenote = (60 * 1000L / bpm) * 4; // this is the time for whole note (in milliseconds) // we need to allocate our array, count the number of commas to get the number of notes int newSongLength = 0; char *tmpPtr = p; while(*tmpPtr) { if(*tmpPtr++ == ',') newSongLength++;} newSongLength++; // num notes = num of commas + 1 newSong->SongLength = newSongLength; // size to pass back newSong->data = (songData*)calloc(newSongLength, sizeof(songData)); // now begin note loop while(*p) { // first, get note duration, if available num = 0; while(isdigit(*p)) { num = (num * 10) + (*p++ - '0'); } if(num) duration = wholenote / num; else duration = wholenote / default_dur; // we will need to check if we are a dotted note after // now get the note note = 0; switch(*p) { case 'c': note = 1; break; case 'd': note = 3; break; case 'e': note = 5; break; case 'f': note = 6; break; case 'g': note = 8; break; case 'a': note = 10; break; case 'b': note = 12; break; case 'p': default: note = 0; } p++; // now, get optional '#' sharp if(*p == '#') { note++; p++; } // now, get optional '.' dotted note if(*p == '.') { duration += duration/2; p++; } // need to convert duration to scale for servo pulses, duration is in milliseconds, divide by 20 // to get the number of servo pulses newSong->data[iNote].duration = (int)duration/20; // need to allocate for newSong // now, get scale if(isdigit(*p)) { scale = *p - '0'; p++; } else { scale = default_oct; } if(*p == ',') p++; // skip comma for next note (or we may be at the end) newSong->data[iNote].note = ((scale + 1) * 12) + (note - 1); // our scale starts at -1, and our indices start at zero, so use scale+1, subtract 1 from result newSong->data[iNote].volume = 10; iNote++; } } }
Intro->SOS->LCD Tutorial->Servo Tutorial->Timer Interrupts->Hacking Servos->Tone generator->DDS Tone Generator->Theremin 1->Theremin 2->RTTL Songs
instead of using a servo can i put the speaker in same pin slot?
Im totally new to this whole arduino thingy
I’m not sure what you are asking? The servo has both an input and output wire in this project, you could hook up a speaker to any pin that is used as an output, just be careful not to draw too much current from Arduino.