Multimedia in Comenius Logo
an Example CD Player

 

Peter Tomcsányi
Comenius University Bratislava,
Slovak Republic,
tel: +421 7 724 826,
email:
tomcsany@internet.sk

Abstract

The paper shows an extensive example how multimedia can be handled in Comenius Logo using the interface of MCI strings.

Keywords

Logo, audio CD player, multimedia, MCI strings

 

1 Introduction

Multimedia are an important part of modern computer programs. In the Microsoft Windows environment there is a language called MCI strings, which can be used to handle multimedia in a uniform way. Comenius Logo includes a command mci, which allows to send MCI strings to Windows. It is quite complicated to understand the language of MCI strings when just reading the list of their commands and their descriptions. Therefore we created a sample application which shows MCI commands in action when creating an application - an audio CD player.

The MCI commands used in this example relate to audio CD, but similar commands can be used to handle other multimedia devices. We did not intend to provide a comprehensive list of all features of the commands. The reader can find these in manuals and help files (sorry, that I do not provide any reference, but I have not any related book available).

2 Let's create a CD player project

Our goal is to create a simple audio CD player project to demonstrate the power of MCI commands when combined with the power of Comenius Logo.

The language of MCI strings is intended to support the user of multimedia devices to control them in a maximally uniform way. There are just a few basic commands, which are understood by all devices. Each device can have its own specific commands. In the next text we will concentrate to the audio CD commands.

When controlling the audio CD player using MCI strings, these steps must be performed:

 

At the end the device must be closed using the close command.

2.1 Simple audio CD commands

Let’s begin with a simple example set of a few Logo procedures using MCI strings which can be used to control the CD player "manually" in direct mode.

to CDprepare
ignore ~
mci [open cdaudio alias cd]
mci [set cd time format tmsf]
end

to CDfinish
CDstop
mci [close cd]
end

to CDPlay
mci [play cd]
end

to CDstop
mci [stop cd]
end

to CDrewind
let "state ~
mci [status cd mode]
mci [seek cd to start]
if :state = "playing [CDplay]
end

to CDseekTrack :n
if or ( :n < 1 ) ~
( :n > mci [status cd ~
number of tracks] ) ~
[stop]
let "state mci [status cd mode]
mci se [seek cd to] :n
if :state = "playing [CDPlay]
end

to CDopenDoor
mci [set cd door open]
end

to CDcloseDoor
mci [set cd door closed]
end

At first CDprepare must be called. It opens the audio CD and sets the time format to tmsf. This means that the time will be reported (in status) and accepted (seek or play) in the form tt:mm:ss:ff where tt is the track number, mm and ss are minutes and seconds measured from the beginning of the track and ff are frames. When some value is equal to zero, it can be omitted.

After CDprepare was called, we can use CDplay to start playing and CDstop to stop playing. CDplay resumes playing at the same place, where it was stopped. CDrewind "rewinds" the CD player to the beginning of the first track. The first and the third line of this procedure causes that if the CD was playing when CDrewind was called, it will play also after rewinding. CDseekTrack seeks to the beginning of a track. CDseekTrack also keeps playing if it was called in playing state. CDOpenDoor and CDcloseDoor open and close the door of the CD player. CDfinish must be called when the CD player will not be used. This is very important, because if it is not called and the device is left open, no other program can access it. The device remains open even if the whole Logo is closed.

So now we can control the CD player from our Logo command line, or we can assign the calls to our commands to the Buttons and use them to control the CD player. This can be done manually using right mouse clicks (see figure 1).

Note that we can define not just simple calls of our procedures, but also more complex statements. For example the commands for the button Forward (which has to seek for the next track) can be defined as CDseekTrack ( mci [status cd current track] ) + 1 and the buttons Track 5 (which play immediately the first track) as CDseekTrack 5 CDplay.

If it is not enough to have directly only the first 5 tracks on the buttons, we can put these functions to the second column of the buttons (this can be done only by a setbuttons command):

to DefineButtons
setbuttons [ [[Start Player] [CDprepare]] ~
[[Finish Player] [CDfinish]] ~
[] [[Play][CDplay]] [[Stop][CDStop]] ~
[[Rewind][CDRewind]] ~
[[Forward ->] [CDseekTrack ~
( mci [status cd current track] ) + 1]] ~
[[<- Back][CDseekTrack ~
( mci [status cd current track] ) - 1]] ~
[[Open door][CDopenDoor]] ~
[[Close door][CDcloseDoor]] ~
[] [] [] [] [] ~
[[Track 1] [CDseekTrack 1 CDplay]] ~
[[Track 2][CDseekTrack 2 CDplay]] ~
[[Track 3][CDseekTrack 3 CDplay]] ~
[[Track 4][CDseekTrack 4 CDplay]] ~
[[Track 5][CDseekTrack 5 CDplay]] ~
[[Track 6] [CDseekTrack 6 CDplay]] ~
[[Track 7][CDseekTrack 7 CDplay]] ~
[[Track 8][CDseekTrack 8 CDplay]] ~
[[Track 9][CDseekTrack 9 CDplay]] ~
[[Track 10][CDseekTrack 10 CDplay]] ~
[[Track 11][CDseekTrack 11 CDplay]] ~
[[Track 12][CDseekTrack 12 CDplay]] ~
[[Track 13][CDseekTrack 13 CDplay]] ~
[[Track 14][CDseekTrack 14 CDplay]] ~
[[Track 15][CDseekTrack 15 CDplay]]]
SetButtonssize []
end

2.2 The player program

The simplest possible player was already done by the set of procedures plus the Buttons Window in the preceding paragraph. What could be improved? Our player should:

 

To achieve the above mentioned functionality, it is not enough to create some procedures and put their calls to buttons. We have to create a main program, which will execute a loop testing all the possible events and react to them. We can still use the Buttons, but when they are used in a program, it is more convenient not to assign whole procedure names to them, just one letter abbreviations, which are then tested inside the program.

The program listed below fulfils the above stated goals and can be treated as an example multimedia Logo project. This example still has one problem - it does not handle error situations. We have omitted the error situations on purpose, because otherwise the program would be too long and much less instructive. A short discussion on handling error situations can be found in the paragraph 4.

The program's Startup command at first tries to finish the CD (this is important when the program has failed for some reason previously) and then calls the CDPlayer command. The CDPlayer command is our main command. It initialises the Buttons (InitButtons), the global variables including textbox turtles (InitVariables) and the CD device (CDprepare). Then it creates a variable "WasNotReady and calls the main body of the program (CDplayerBody). After the body finishes, the CD device is closed (CDfinish).

The CDplayerBody command is the main program loop. At first it tests for keystrokes - they can result from pushing a button, typing the key on the keyboard or using some mouse actions (in our case just -3 which means that the slider was changed by a mouse action). For each keyboard action an appropriate command is called. After testing the keyboard TrackCDState is called and the loop is repeated through a recursive call to CDplayerBody.

SeekMilliseconds is a command similar to CDseekTrack, just it its input is not a track number but a the number of milliseconds from the beginning of the whole CD.

The command TrackCDState is responsible for showing the current state on the screen. At first it senses if the CD drive is ready. If not, then it sets a flag "WasNotReady and exits. If it is ready, but the WasNotReady flag is on, then it supposes that CD media were replaced, therefore it calls ChangeCD. Then the TimePos slider is updated, but this is done only when the left mouse button is not down (i.e. the slider is not being manipulated by mouse at that moment). As the slider shows the whole CD, it is easier to deal with milliseconds in it than with time in tmsf format. Therefore the time format is switched to milliseconds, the position is read to TimePos variable (which is connected to the slider on the screen) and the time format is set back to tmsf (this is needed for the rest of the program). The actual position in milliseconds is divided by 100 because the range of a slider is limited by the number 32767. The last thing to do is to show the name of the current track. It is supplied by the operation NameForTrack and it is assigned to the variable TrackName which is connected to a textbox turtle on the graphics screen.

ChangeCD at first creates a list consisting of start positions of all tracks of the current CD and places it to a global variable ti. At the end of the list the total length of the CD is placed. This list characterises the currently inserted CD media (It may be strange, but the CD does not contain its name and the names of its tracks in textual format). This identification is then sought in the list of known CDs (variable KnownCDs). The name of the current CD is set either to "(Unknown CD)" or to a name found in the global variable CDnames. The position in the list is put to the global variable CDnum. If it is 0 then it is an unknown CD. Then the lower and upper limit for the slider TimePos are set, the names of tracks are written to the Buttons (UpdateTrackNames) and a scale indicating the beginning of each track is painted under the TimePos slider (PaintScale). Then the 12-th button of the Button window is set to empty if the CD is an unknown one or to "Edit tracks" otherwise.

The NameForTrack operation outputs the textual name of a track. If the currently inserted CD is unknown or its track names were not written yet, or it is no name set to the current track, then the name will be "Track n" otherwise the name is found in the global variable TrackNames.

The command EditCDName allows the user to type in a name for the current CD. If the current CD is an unknown one, then the value of ti is appended to the list of known CDs, the new name is appended to the list of names (variable CDnames) , an empty list is appended to the list of track names and the CDnum variable is updated. If the current CD is a known one, then its name stored in CDnames is updated. Afterwards the 12-th button is set to allow editing of track names (it is allowed only for known CDs).

The command EditTrackNames allows the user to edit the names of the tracks on the current CD. It extracts the track names for the currently inserted CD from the list TrackNames to the variable EditNames. This variable is connected to a textbox turtle which is normally hidden. Just now it is shown and edited. When the user finishes editing, the content of EdiNames is put back to the appropriate place to TrackNames.

to Startup
catch "error [CDfinish]
CDplayer
end

to CDplayer
let "WasNotReady "false
InitButtons
InitVariables
CDprepare
ChangeCD
CDplayerBody
CDfinish
end

to InitButtons
setbuttons [[[Exit Player][#x]][] ~
[[Play][#p]][[Stop][#s]][[Rewind][#r]] ~
[[Forward - >][#f]][[< - Back][#b]] ~
[[Open door][#o]][[Close door][#c]] ~
[][[Edit CD name][#n]][[Edit tracks][#e]]]
setbuttonssize []
showbuttons
end

to InitVariables
make "CDname []
maketurtle "CDname [-335 193 0]
ask "CDname [setboxmode "true setbox [pp 3] st]
make "TrackName []
maketurtle "TrackName [-335 134 0]
ask "TrackName [setboxmode "true setbox [pp 3] st]
make "EditNames []
maketurtle "EditNames [-214 186 0]
ask "EditNames [setboxmode "true setbox [pp 4 size [20 15]]]
make "timepos 0
maketurtle "TimePos [-382 59 0]
ask "TimePos [setboxmode "true setbox [pp 6 size [60 1] title 0] st]
if ( or not name? "CDnames not name? "KnownCDs not name? "TrackNames ) ~
[make "CDnames [] make "KnownCDs [] make "TrackNames []]
end

to CDplayerBody
if key? ~
[let "x readkey ~
if :x > 0 ~
[case char :x ~
[1 2 3 4 5 6 7 8 9 : ; < = > ? [CDseekTrack :x - ascii "0 CDPlay] ~
S s [CDstop] ~
R r [CDrewind] ~
P p [CDPlay] ~
X x [CDstop stop] ~
O o [CDopenDoor] ~
C c [CDcloseDoor] ~
B b [CDseekTrack ( mci [status cd current track] ) - 1] ~
F f [CDseekTrack ( mci [status cd current track] ) + 1] ~
N n [EditCDName] ~
E e [EditTrackNames]]] ~
[if :x = -3 [waituntil [( .and first mousestate 1 ) = 0] ~
( readkey 0 ) SeekMilliseconds :timepos * 100]]]
TrackCDState
CDplayerBody
end

to SeekMilliseconds :n
mci [set cd time format milliseconds]
if and ( :n >= 0 ) ( :n <= mci [status cd length] ) ~
[let "state mci [status cd mode] ~
mci se [seek cd to] :n ~
if :state = "playing [CDPlay]]
mci [set cd time format tmsf]
end

to TrackCDState
if not mci [status cd ready][make "WasNotReady "true stop]
if :WasNotReady [ChangeCD]
make "WasNotReady "false
if ( .and first mousestate 1 ) = 0 ~
[mci [set cd time format milliseconds] ~
make "Timepos ( mci [status cd position] ) / 100 ~
mci [set cd time format tmsf]]
make "TrackName NameForTrack mci [status cd current track]
end

to ChangeCD
let "nt mci [status cd number of tracks]
mci [set cd time format milliseconds]
make "ti []
repeat :nt [make "ti lput mci se [status cd position track] repc :ti]
make "ti lput mci [status cd length] :ti
make "CDnum member :ti :KnownCDs
make "CDname if :CDnum = 0 [[( Unknown CD )]][item :CDnum :CDnames]
ask "TimePos [setbox list "from list ~
round ( mci [status cd position track 1] ) / 100 ~
round ( mci [status cd length] ) / 100]
UpdateTrackNames
mci [set cd time format tmsf]
PaintScale
( setbuttons 12 if :CDnum = 0 [[]][[[Edit tracks][#e]]] )
end

to PaintScale
clean
tell 0 ht seth 0
settt [FixedSys][]
let "x0 first :ti
let "k ( ( first ask "timepos [getbox "asize] ) * ~
( first textsize "M ) - 2 - 15 ) / ( ( last :ti ) - :x0 )
repeat ( count :ti ) - 1 [pu setpos ( ask "timepos [pos] ) + ~
list ( ( item repc :ti ) - :x0 ) * :k + 2 -30 ~
pd fd 10 ~
pu bk 10 lt 90 fd 0.5 * first textsize repc rt 90 tt repc]
end

to UpdateTrackNames
let "mm if count :ti > 16 [15][( count :ti ) - 1]
repeat :mm [( setbuttons repc + 15 ~
list NameForTrack repc ( list word "# char repc + 48 ) )]
while [:mm < 15][inc "mm ( setbuttons :mm + 15 [] )]
end

to NameForTrack :i
if :CDnum = 0 [output list "Track :i]
if count :TrackNames = 0 [output list "Track :i]
if ( count item :CDnum :TrackNames ) < :i [output list "Track :i]
output item :i item :CDnum :TrackNames
end

to EditCDName
let "name ( rld [Write the title of the CD currently inserted] ~
[Name this CD][] :CDname )
if empty? :name [stop]
if :CDnum = 0 ~
[make "KnownCDs lput :ti :KnownCDs make "CDnames lput :name :CDnames ~
make "TrackNames lput [] :TrackNames make "CDnum count :KnownCDs] ~
[make "CDnames replace :CDnum :CDnames :name]
make "CDName :name
( setbuttons 12 [[[Edit tracks][#e]]] )
end

to EditTrackNames
if :CDnum = 0 [stop]
make "EditNames item :CDnum :TrackNames
ask "EditNames [st]
edit ":EditNames
ask "EditNames [ht]
make "TrackNames replace :CDnum :TrackNames :EditNames
UpdateTrackNames
end

Figure 3 A snapshot of the screen with our CD player

3 Synchronous and asynchronous operation

Until now all the MCI commands used to control the audio CD player were issued as asynchronous ones. This means that when Logo has sent a command, the CD player driver has performed it and immediately returned to Logo regardless if the operation has finished or not. Therefore when mci [play cd] is executed, it just starts playing and returns to Logo.

This was good for our CD player project, but sometimes we would like to begin play one song (track), then wait until it finishes and only after that make some other actions. Sometimes we would even need to begin to play a song and simultaneously do some other things in the program but still be able to react when the song finishes. These two modes of operation will be described in the two next paragraphs

3.1 Waiting for completion

There are two ways how to wait for completion of an MCI command's effect. We will show this on the next two commands. Both of them play one track and wait for the completion:

to PlayTrackAndWait :n
mci (se [play cd from] :n [to] :n+1 [wait])
end

to PlayTrackAndWait2 :n
mci (se [play cd from] :n [to] :n+1)
waituntil [(mci [status cd mode]) <> "playing]
end

The PlayTrackAndWait command uses a wait parameter, which can be used with MCI play command. The wait parameter can be used with other MCI commands ac well. It tells to the MCI driver not to return after the operation is started but only when it is finished. PalyTrackAndWait2 uses another technique called busy waiting. Logo issues a normal play command and then it loops in a waituntil loop while the MCI driver does not say that the CD is not playing anymore. The first way is used when the program needs not make any actions during the CD is playing. The second one is used when the program has to perform some single action (like blinking a light on the screen) during the CD is playing.

3.2 Asking to be notified

The methods of waiting are sometimes it not sufficient if a program wants to play some song in the background while the program performs some complicated actions, but still wants to react if the song finishes. The simplest example of such a situation is if we want to play a song in the background repeatedly during our program runs.

For this purpose another parameter can be used in an MCI command - the notify parameter. In this cause the mci procedure must have a second input (we will call it notify commands), which is a list of commands to perform when the effect of the command is finished.

to RepeatPlaying :n
if :n = 0 [stop]
make "Song :n
(mci (se [play cd from] :n [to] :n+1 [notify]) [RepeatPlaying :song])
end

The above command will begin to play a song specifying the list [RepeatPlaying :song] as the action to perform when it finishes. So the song will be played again and again specifying [RepeatPlaying :song] as the next action and so on, and so on. Note that the input parameter n was stored to a global variable Song. This is because the notify commands will be executed sometimes after the RepeatPlaying command finished, therefore its parameter n will be not available.

Before Using RepeatPlaying for the first time, we have to call CDprepare and at the end of the program CDfinish must be called.

Now if we call RepeatPlaying 3 and afterwards cdstop, we will observe that the CD does not stop, instead the 3-rd song will be played again. This is because the notify commands are invoked not only when the play command finishes normally, but also if it is interrupted somehow. To avoid this behaviour, we have to set the global variable song to 0 before stopping the CD. This can be done inside a modified CDstop command:

to CDstop
make "song 0
mci [stop cd]
end

Similarly also other our command (like CDrewind, CDseekTrack, CDopenDoor, ...) should use this trick to disable the notify commands to repeat playing the selected background song.

4 Handling error situations

In situations when a professional program controls some hardware, it should always test fro error situations. In this paragraph we will briefly mention, how it can be done in our CD player program.

An error can occur in each mci call. We can handle the error situation in three ways: abort the program, report the error and continue (when an output from mci is needed, then a default value must be supplied) or not report any error and continue (when an output from mci is needed, then a default value must be supplied)

We can create commands which substitute the mci command (i.e. they can be used instead of mci), test for the errors and handle them in the ways described above. As mci can be used both as a command and as an operation, we would need to create 6 new procedures (3 commands and three functions). We will demonstrate just some of them (ShowError is a new command which shows the error text and returns true, if there was any error):

to ShowError :title
let "e error
if :e = [][output "false]
( messagebox item 2 :e :title )
output "true
end

to mci1 :com
catch "error [mci :com]
if ShowError [MCI problem] ~
[catch "error [CDfinish] ~
throw "TopLevel]
end

to mci1f :com
catch "error [output mci :com]
if ShowError [MCI problem] ~
[catch "error [CDfinish] ~
throw "TopLevel]
end

to mci2 :li
catch "error [mci :li]
ignore ShowError [MCI error]
end

to mci3f :com :def
catch "error [output mci :com]
ignore error
output :def
end

 

Using the above listed procedures we can now rewrite some of the procedures used in our CD player program:

to CDprepare
ignore mci1f [open cdaudio alias cd]
mci1 [set cd time format tmsf]
end

to CDPlay
mci2 [play cd]
end

to CDrewind
let "state mci3f [status cd mode] "stopped
mci2 [seek cd to start]
if :state = "playing [CDplay]
end

5 Conclusion

The methods for handling the CD audio player from Comenius Logo using MCI strings were illustrated on a simple project. Similar players for other multimedia formats (like waves, MIDI files or AVI files) can be constructed similarly. We hope that this paper can help the reader to understand better both Comenius Logo and MCI strings.