Runtime Revolution
Articles Other News

Quick Apps with Rev: The Singing Canary

by Jacqueline Landman-Gay

You can download the stack and canary songs used in this article here.

I have a ditzy canary that was taken from his parents too early. Male canaries need to learn to sing, and during their first year they listen to other males, extract portions of various songs, and recombine pieces into a song that is their own. This song identifies them to other canaries and every male's song is unique.

Unfortunately my little guy had no one to learn from, so he has been belting out songs copied from wild birds he hears through the window, in particular, the mourning doves that hang out on our roof. I heard him practicing carefully for weeks ("ooooh-oo-oo. Oo. Oo." Sung with a little trill.) It's very funny, but not really a song that a decent canary would be proud of.

I looked at CDs you can purchase to teach your canary to sing, but figured I could do better. So I downloaded some sample mp3 canary files from a web site, tossed in a few I recorded from my previous canary, and made a little stack that plays back random songs at random time intervals. It took me about 20 minutes to write this little Canary iTunes Shuffle.

To start, I created a stack with a field, a player, and a button with this handler:

on mouseUp
if the label of me = "start" then
set the label of me to "Stop"

set the label of me to "Start"

end if

end mouseUp

And in the card script, a preOpenCard handler loads a list of all mp3 files contained in a folder called "songs":

on preOpenCard
put "" into fld "next play"
set the hilitedlines of fld "playlist" to 0
get songPath()
set the directory to it
put the files into tSongs
filter tSongs with "*.mp3"
if tSongs = "" then put "No songs found." into tSongs
put tSongs into fld "playlist"

end preOpenCard

function songPath
-- mp3 files are stored in a folder called "songs".
-- Calculate and return the base path of this folder.
put the filename of this stack into tPath
set the itemdel to slash
put "songs" into last item of tPath
return tPath & slash

end songPath

Now when the stack opens, it will show all available song files in the list.

All the real action takes place in the card script. The Start button handler above sends a "playSong" message to the card and changes its label to "Stop". The "playsong" handler in the card script chooses a random line in the list, calculates the file path using the "songPath" function, sets the player filename to the filepath, and starts the player:

on playSong
-- randomly choose an mp3, play it at a random time
put any line of fld "playlist" into tSongName
put songPath() & tSongName into tSong
lock messages
set the filename of player "canary" to tSong
unlock messages
set the hilitedlines of fld "playlist" to lineoffset(tSongName,fld "playlist")
set the currentTime of player "canary" to 1
start player "canary"

end playSong

Whenever a player finishes playing its content, Revolution sends a "playStopped" message. The card catches this message, calculates a random time period, and sends a message to itself in that amount of time, telling itself to "playsong" again:

on playStopped pPlayer
set the hilitedlines of fld "playlist" to 0 -- esthetic
put random(60) into tSecs
if "playSong" is not in the pendingmessages
then send "playSong" to me in tSecs seconds

end playStopped

At first, I hard-coded a 60-second maximum time interval into the script as per above. That meant that the next song would play at a random interval, though it would never exceed one minute.

I decided it would be preferable to have an option button that would allow me to choose the maximum time interval between songs. In the menu contents of the option button, I made sure that the first "word" of each line in the button's menu contents was a number, representing a number of seconds. This way, the script only needed to read the first word of the menu selection and could use that number directly when calculating the next random time interval. No other time calculations were necessary, and I only had to change a single line of the handler. Here's the revised handler with line 2 revised:

on playStopped pPlayer
set the hilitedlines of fld "playlist" to 0
put random(word 1 of the label of btn "Lag") into tSecs
if "playSong" is not in the pendingmessages
then send "playSong" to me in tSecs seconds

end playStopped

When the user wants to stop the automatic playback, the other half of the Start button handler turns off the player and removes any pending messages. It also changes its label back to "Start" so it is ready to go again. The "stopSongs" handler looks like this:

on stopSongs -- stop playback
lock messages -- prevent "playstopped" from being sent
stop player "canary"
put "" into fld "next play"
set the hilitedlines of fld "playlist" to 0
repeat for each line tLine in the pendingmessages
if tLine contains "playSong" then cancel (item 1 of tLine)
end repeat
unlock messages

end stopSongs

After a few test runs, it became clear it would be handy to know when the next song was due to be played. This is the nice thing about these quick little Revolution tools -- you can tinker and add things as you go along. I dropped in a label field to hold the time display. Calculating the display time was simple; since the calling handler had already chosen a random number of seconds till the next playback, this integer was passed as a parameter to the "displayTime" handler. The revised playStopped handler and the displayTime calculation now looked like this:

on playStopped pPlayer
set the hilitedlines of fld "playlist" to 0
put random(word 1 of the label of btn "Lag") into tSecs
displayTime tSecs
if "playSong" is not in the pendingmessages
then send "playSong" to me in tSecs seconds

end playStopped

on displayTime pSecs
get the long time
convert it to seconds
add pSecs to it
convert it to long time
put "Next song at:" && it into fld "next play"

end displayTime

All that was left was to add a closeCard handler that called the "stopSongs" handler to stop any playback and pending messages before the stack closed.

And that's it -- a few handlers plus a control button and I had a little mini iTunes randomizer. Of course, after I was asked to submit this as an article I wanted to improve the appearance a little bit, so I resized the fields, added a photo, and gave the card a background color -- only a few seconds of work. I also added a short handler in the list field that allows you to double-click on any song to play it on demand. See the field script in the example stack for that.

I've been running this stack periodically and my little canary is responding. The house is full of bird song, it's lovely. Even if you don't have a canary to train, you might enjoy listening to the birdsongs anyway. They are almost as pretty as a real bird, with the advantage that the stack doesn't scatter seed all over the floor.

©2005 Runtime Revolution Ltd, 15-19 York Place, Edinburgh, Scotland, UK, EH1 3EB.
Questions? Email for answers.