One of the features that I introduced in the latest version of pbForth for the by LEGO Mindstormsis the ability to play arbitrary tones to produce music. That being said, we can't set the bar for the quality of the music very high. Thanks go to Guido Truffelli of the Italian LEGO User's Group for providing the motivation and sample song data for this article.
This article is all about making the RCX play simple songs, and it's surprising what we can do with just a little bit of work. The key concept you'll pick up in this article is setting up a data structure in pbForth, and devising a scheme to loop through a series of entries to produce a song. You will also learn about a new feature I added to pbForth as of version 2.1.4 - a simple background task.
I'm assuming that you have at least a basic grasp of programming in pbForth. You should also know how to upload scripts to the RCX. You might also review #error - glossary pbForth RCX Extension Scripts not found if you're unfamiliar with the special capabilities of pbForth on the RCX.
To get the most up to date version of pbForth firmware and the example scripts, you can get the pbForth Scripts as a ziparchive or the pbForth Scripts as a tar.gzarchive. The scripts you'll need are in the rcxMusic directory.
Forth exposes the hardware of the target processor right \"down to the metal". This means that the programmer must be aware of the organization of his data. Wherever possible, you should try to encapsulate the data of the application. In this case, the basic idea is that we will loop through a series of (time,frequency) pairs and sound the tone that goes with each one. When we get to a terminating entry with a time of 0 we know the song is over.
The first thing we need to do is figure out the best way to represent the (time,frequency) pairs in memory. For our first cut at the problem, let's represent them using 16-bit values. In pbForth, 16 bits is a CELL. In fact, in any Forth a CELL is the most \"efficient" representation of a value. In any case, we'll use one CELL for the time, and another one for the frequency for a total of 4 bytes per note.
The next thing we'll need is a way to get those values into memory. We could allocate some contiguous memory for each song, or we could just lay down the notes as we go. I prefer the second approach. When we lay down values in memory, we are actually \"compiling" into the dictionary space. The convention in Forth is to end compiling words in a \"," (comma).
To complete our first attempt at playing music, we'll need to keep track of where in the song we are pointing, a way to fetch the next note from memory, and some kind of loop that waits until the current note is finished before we play the next one. Here's the sample code:
\ ----------------------------------------------------------------------------- \ music1.txt - simple method of playing music on the RCX \ ----------------------------------------------------------------------------- \ Revision History \ \ R. Hempel 2002-05-20 - Original \ ----------------------------------------------------------------------------- \ Thanks to Guido Truffellifor the inspiration and \ the sample music file. \ \ The SOUND_TONE word can be used to play arbitrary tones on the RCX \ speaker. This script shows how to set up a simple data structure to \ handle playing the tones. \ \ New dictionary entries: \ \ CREATE_SONG songname Creates a new song called "songname" \ NOTE, ( time freq -- ) Adds a new note to the song \ END_SONG ( -- ) Adds a terminating entry to the song \ songname PLAY_SONG Plays "songname" \ ----------------------------------------------------------------------------- BASE @ DECIMAL : CREATE_SONG CREATE ; \ Makes a new word in the dictionary. Returns the \ address of the memory following the entry. : NOTE, ( time freq -- ) \ Compile a new (time,freq) pair into dictionary space , , ; : END_SONG 0 0 NOTE, ; \ Compile a terminating entry into the dictionary space : NOTE@ ( addr -- time freq ) \ Get the (time,freq) pair we are pointing to DUP CELL+ @ \ Get the time SWAP @ ; \ Get the frequency : NEXT_NOTE ( addr -- naddr ) \ Point to the next note [ 2 CELLS ] LITERAL + ; : PLAY_SONG ( addr -- ) BEGIN DUP NEXT_NOTE \ Point to the next entry in the song SWAP NOTE@ \ Get the entry OVER \ Check the time WHILE \ If non-zero, then proceed SOUND_TONE \ Sound the tone and spin BEGIN RCX_SOUND DUP SOUND_GET @ 0= UNTIL REPEAT ; BASE ! \ -----------------------------------------------------------------------------
Note the use of two loops in the PLAY_SONG script. The first one is a BEGIN-WHILE-REPEAT construct that loops through all of the notes and plays them until it gets to one with a time of 0. The other loop is started once SOUND_TONE is executed. It uses a BEGIN-UNTIL structure that spins until it seesthat the current tone has finished playing. The song looks something like this in memory:
| Song Title | |
| time[0] | frequency[0] |
| time[1] | frequency[1] |
| time[...] | frequency[...] |
| 0 | 0 |
After you upload this sample script, and then the companion script called \"rcxMusic/circleSong.txt" you will be ready to play your first song by typing:
CIRCLE PLAY_SONG
What could be easier?
If you look at the \"rcxMusic/circleSong.txt" script, you'll see what's going on. The CREATE_SONG word lays down a dictionary entry for CIRCLE. The NOTE, entries that follow \"compile" the (time,frequency) pairs into the dictionary space. When you type CIRCLE, it returns the address of the next available cell after CIRCLE was created, which is where the notes are. The PLAY_SONG word just plays the notes we compiled until it gets to the last note.
Of course, this is where you come to the realization that this simple approach might have some drawbacks. Those of you that are experienced programmers will immediately recognize that the \"spin-loop" will prevent the RCX from doing anything else while playing the song. In some cases this will be OK, but what if we want the RCX to whistle a happy song as it follows a line and then buzz when it drives off the line?
The background music player uses the same technique described in the article on HowTo Make An RCX Monitor. Vectored execution makes it possible to run the music player in the background.
The article on making a background monitor describes how the 'UserIdle vector makes use of the spare CPU cycles that are available when pbForth is waiting for input. This time, we'll use another vector that is even more powerful, and only slightly more dangerous - the 'UserISR vector.
The main differences between the two are that 'UserISR runs 1,000 times a second, and that it has its own stack context. This means we can't count on anything hanging around on the stack from one run of the ISR to the next. We'll need to modify our music player, but only slightly to take this into account.
Another thing we'll do is save some space in the music tables by realizing that the maximum allowed value for a note time is 255*10msec. Those of you with some programming experience will know that the value of 255 is the largest unsigned number that can be contained in a byte. And that means we only need to use 3 bytes per note instead of 4. On small embedded systems, you save memory any way you can. The neat thing is that because we were careful in deciding how to define songs, we won't have to rewrite all of our song files - only the player needs to change!
\ ----------------------------------------------------------------------------- \ music2.txt - background method of playing music on the RCX \ ----------------------------------------------------------------------------- \ Revision History \ \ R. Hempel 2002-05-20 - Original \ ----------------------------------------------------------------------------- \ Thanks to Guido Truffellifor the inspiration and \ the sample music file. \ ----------------------------------------------------------------------------- BASE @ DECIMAL 0 VALUE NOTE_ADDR \ The current address of the note we are playing : CREATE_SONG CREATE \ Makes a new word in the dictionary. DOES> \ Returns the address of the memory following the entry TO NOTE_ADDR ; \ and copies it to the NOTE_ADDR pointer : NOTE, ( time freq -- ) \ Compile a new (time,freq) pair into dictionary space , C, ; \ Note that C, compiles a byte into the dictionary! : END_SONG 0 0 NOTE, ; \ Compile a terminating entry into the dictionary space : NOTE@ ( addr -- time freq ) \ Get the (time,freq) pair we are pointing to DUP CELL+ C@ \ Get the time SWAP @ ; \ Get the frequency : NEXT_NOTE ( addr -- naddr ) \ Point to the next note [ 1 CHARS 1 CELLS + ] LITERAL + ; \ This version is ready to play songs in the background. Instead of spinning around \ to check if a note has finished, it checks once and if there is no note playing moves \ on to the next note. : PLAY_SONG ( -- ) RCX_SOUND DUP SOUND_GET @ 0= \ Check if a note is NOT playing IF NOTE_ADDR DUP NOTE@ \ Get the next note \ Stack: ( addr time freq -- ) OVER \ Check the time IF SOUND_TONE \ If not zero, play the note NEXT_NOTE TO NOTE_ADDR \ and advance to the next note... ELSE DROP 2DROP \ Otherwise, just clean up the stack THEN THEN ; \ The following line was uncommented after extensive testing of PLAY_SONG \ from the command line... ' PLAY_SONG TO 'UserISR BASE ! \ -----------------------------------------------------------------------------
You should also reload the sample song \"rcxMusic/circleSong.txt" because the data structure is changed. The old version in uses 4 bytes per entry while the new one uses 3 bytes. I tested this version of PLAY_SONG extensively by executing it from the command line to make sure it had no side effect on the stack. The source script automatically loads it into the background vector for you.
If you actually read the code, you'll see that the CREATE_SONG word has changed. It still creates the dictionary entry at compile time, but now it has the DOES> construct too.
This is one of the neatest features of Forth - DOES> specifies what the word we just created will do at \"run-time" in addition to the default action of putting its address on the stack. In this case, we're saying it should set the value of NOTE_ADDR to the address left on the stack. This will point to the beginning of the song and PLAY_SONG will start automatically! One application of this feature is that you could change songs on the fly by just typing the new song name that you want to play.
This article described two ways to play music on the RCX. The first is a simple word that spins through a structure of notes but does not allow anything else to happen on the RCX. The second technique uses the concept of vectored execution - first described in #error - glossary HhowtoRCXMonitor1 not found- and uses a new vector called 'UserISR that runs 1,000 times a second.
Many of you will realize that PLAY_SONG probably doesn't need to run 1,000 times a second. Future articles will show you ways to cut down on the number of times it has to run and will pave the way for more intersting uses of the background ISR vector, including a very simple co-operative multi-tasker!