Return to the Index
<==

Advanced Topics

Many of the subjects covered in this chapter may provide benefits for all levels of programmers.

The subjects become progressively more technical, however. This manual cannot cover every OPL keyword in detail, as some give access to the innermost workings of the Series 3a. Some of these keywords are touched on in this chapter.


Programs, modules, procedures

Programs using more than one module

Program is the more general word for the "finished product" a set of procedures which can be run. A program may consist of one module containing one or more procedures: (picture)

It may also consist of a number of modules containing various procedures: (picture)

A procedure in one module can call procedures in another module, provided this module has been loaded with the LOADM command. LOADM needs the filename of the module to load.

For example, if an OPL module "MAIN" has these two procedures:

and a module called "LIBRARY" has this one:

then running "MAIN" would display "Running pthis:" and "in this module"; then display "Running pother:" and "in module called LIBRARY"; and then display "Finished".

You would usually only need to load other modules when developing large OPL programs.

You can use LOADM up to three times, to load up to three modules. If you want to load a fourth module, you must first unload one of those currently loaded, by using UNLOADM with the filename. To keep your program tidy, you should UNLOADM a module as soon as you have finished using it.

Note : If there is an error in the running program, you will only be positioned in the Program editor to the place where the error occurred if you were editing the module concerned (and you ran the program from there).
In spite of its name, LOADM does not "load" the whole of another module into memory. Instead, it just loads a block of data stored in the module which lists the names of the procedures which it contains. If such a procedure is then called, the procedure will be searched for, copied from file into memory and the procedure will then be run.

The modules are searched through in the order that they were loaded the module loaded last is searched through last. You may make considerable improvements in speed if you have few modules loaded at a time (so avoiding a long search for each procedure) and if you load the modules with the most commonly called procedures first.

Cacheing procedures for speed

Without procedure cacheing, procedures are loaded from file whenever they are called, and discarded when they return. This is true even when procedures are all in one module. (With more than one module, LOADM simply loads a map listing procedure names and their positions in the module file so that they can be loaded fairly efficiently. It does not load procedures into memory.)

If a well-designed program calls procedures regularly, you can speed it up by cacheing procedures keeping the code for a procedure loaded in memory after it returns, so that if it is called again there is no need to fetch it from file again. The CACHE command takes two arguments the initial size of the cache and the maximum size (both in bytes). These can be up to 32,767 bytes. The minimum in both cases is 2000 bytes.

For a small program, you might use "CACHE 2000,2000" at the start of the program. Up to 2000 bytes of procedure code will be cached. If the cache fills up, and a procedure is called which is not in the cache, space will be made for it in the cache by removing other procedures from it.

For a much larger program, you might use "CACHE 10000,10000". You may wish to change the settings and find the smallest setting which produces the maximum speed improvement.

Once a cache has been created, "CACHE OFF" prevents further cacheing, although the cache is still searched when calling subsequent procedures. "CACHE ON" may then be used to re-enable cacheing.

Calling procedures by strings

Procedures can be called using a string expression for the procedure name. Use an "@" symbol, optionally followed by a character to show what type of value is returned for example, "%" for an integer. Follow this with the string expression in brackets. You can use upper or lower case characters.

Here are examples showing the four types of value which can be returned:

i%  = @%(name$):(parameters) for integer
l&  = @&(name$):(parameters) for long integer
s$  = @$(name$):(parameters) for string
f   = @(name$):(parameters) for floating-point
So, for example, "test$:(1.0,2)" and "@$("test"):(1.0,2)" are equivalent.

Note that the string expression does not itself include a symbol for the type ("%", "&" or "$").

You may find this useful if, in a more complex program, you want to "work out" the name of a procedure to call at a particular point. The chapter on `Friendlier interaction' includes an example program which uses this method.


Where files are kept

The internal memory and SSDs use a DOS-compatible directory structure, the same as that used on disks on business PCs. However, the Series 3a hides the complexities of this. You have only to supply a filename, and to say whereabouts a file is internal, on an SSD, or on another computer to which the Series 3a is linked.

In fact, in order to completely specify a file, the Series 3a uses a filing system, drive (or device), directory and filename:

To completely specify a file, you add the four parts together. The directory part must end with a backslash. So an OPL module named "TEST", in a directory called "\JO" in the Series 3a internal memory can be specified as "LOC::M:\JO\TEST.OPL". If this file were in the "\JO\BACKUP" directory, it would be completely specified as "LOC::M:\JO\BACKUP\TEST.OPL". If it were in the root directory, you would specify it as "LOC::M:\TEST.OPL".

A full file specification may be up to 128 characters long.

In OPL, unless you say otherwise, files are kept on the Series 3a "(LOC::"), in the internal memory ("M:"). The directories and file extensions used are:

Type of file	      Directory	     File extension 

OPL modules \OPL .OPL translated modules \OPO .OPO data files \OPD .ODB bitmaps \OPD .PIC

Using file specifications in OPL

OPL commands which specify a filename, such as OPEN, CREATE, gLOADBIT and so on, can also use any or all of the other parts that make up a full file specification. (Normally, the only other part you might use is the drive name, if the file were on an SSD.) So for example, "OPEN "REM::C:\ADDR.TXT""... tries to open a data file called "ADDR.TXT" on the root directory of a hard disk "C:" on an attached PC.

You can use the PARSE$ function if you need to build up complex filenames. See the alphabetic listing for more details of PARSE$.

Unless you have a good reason, though, it's best not to change directories or file extensions for files on the Series 3a. You can lose information this way, unless you're careful.

Control of directories

Use the MKDIR command to make a new directory. For example, "MKDIR "M:\MINE\TEMP"" creates a "M:\MINE\TEMP" directory, also creating "M:\MINE" if it is not already there. An error is raised if the chosen directory exists already use "TRAP MKDIR" to avoid this.

SETPATH sets the current directory for file access for example, "SETPATH "A:\DOCS"". LOADM continues to use the directory of the running program, but all other file access will be to the new directory instead of \OPD.

Use RMDIR to remove a directory for example, "RMDIR "M:\MINE"" removes the "MINE" directory on "M:\". An error is raised if the directory does not exist use "TRAP RMDIR" to avoid this.

You can only remove empty directories.

File specifications on REM::

You should not assume that remote file systems use DOS-like file specifications for example, an Apple Macintosh specification is "disk:folder:folder:filename". You can only assume that there will be four parts disk/device, path, filename and (possibly null) extension. PARSE$, however, will always build up or break down REM:: file specifications correctly.


Safe pointer arithmetic

Whenever you wish to add or subtract something from an integer which is an address of something (or a pointer to something) you should use UADD and USUB. If you do not, you will risk an `Integer overflow' error.

An address can be any value from 0 to 65535. An integer variable can hold the full range of addresses, but treats those values from 32768 to 65535 as if they were -32768 to -1. If your addition or subtraction would convert an address in the 0-32767 range to one in the 32768-65535 range, or vice versa, this would cause an `Integer overflow'.

UADD and USUB treat integers as if they were unsigned, ie as if they really held a value from 0 to 65535.

For example, to add 1 to the address of a text string (in order to skip over its leading byte count and point to the first character in the string), use "i%=UADD(ADDR(a$),1)", not "i%=ADDR(a$)+1".

USUB works in a similar way, subtracting the second integer value from the first integer, in unsigned fashion for example, "USUB(ADDR(c%),3)".

Note : "USUB(x%,y%)" has the same effect as "UADD(x%,-y%)".

OPL applications (OPAs)

You can make an OPL program appear as an icon in the system screen, and behave like the other icons there, by converting it into an OPL application, or OPA. There are five different types of OPA, called type 0 to type 4:

When you pick a new file to use, its name becomes bold, and the one that was previously bold reverts to normal. What has actually happened is that the running OPA has switched files it has not closed down, and no new copy of the OPA is run.

When you select a new file, one of the running OPAs normally switches to this file, as with type 2 OPAs. You can, however, with Shift-Enter, start a new OPA running just for this file, without a different file exiting.

Types 3 and 4 allow more than one file to be in use (ie have their names in bold). When this happens a separate version of the OPA runs for each bold file. With types 0, 1 and 2, only one version of the OPA can be running at any time.

Initially, the OPA's name appears beneath the icon. If you move onto this name and press Enter, file-based OPAs (types 1 to 4) will use a file of this name. Types 2, 3 and 4 allow you to create lists of files below the icon (with the `New file' option). You use the file lists in the same way as the lists under the other icons in the System screen.

You can stop a running OPA by moving the cursor onto its bold name and pressing Delete. After a `Confirm' dialog, the System screen tells the OPA to stop running.

Defining an OPA

To make an OPA, your OPL file should begin with the APP keyword, followed by a name for the OPA. The name should begin with a letter, and comprise of 1 to 8 letters and/or numbers. (Note that it does not have quote marks.) The APP line may be followed by any or all of the keywords PATH, EXT, ICON and TYPE. A Series 3a OPA should also add $1000 to the type if it has its own 48x48 pixel, black/grey icon (see the discussion of ICON below for details). Finally, use ENDA, and then the first procedure may begin as in a normal OPL file. Here is an example of how an OPA might start:

APP Expenses
  TYPE $1003
  PATH "\EXP"
  EXT "EXP"
  ICON "\OPD\EXPENSES.PIC"
ENDA
Here is another example:
APP Picture
  TYPE 1
ENDA
TYPE takes an integer argument from 0 to 4. The various types of OPA are outlined earlier. If you don't specify the type, 0 is used.

PATH gives the directory to use for this OPA's files. If you do not use this, the normal "\OPD" directory will be used. The maximum length, including a final "\", is 19 characters. Don't include any drive name in this path.

EXT gives the file extension of files used by this OPA. If you do not specify this, ".ODB" is used. Note that the files used by an OPA do not have to be data files, as the I/O commands give access to files of all kinds. EXT does not define the file type, just the file extension to use. However, for simplicity's sake, examples in this section use data files.

(PATH and EXT provide information for the System screen they do not affect the program itself. The System screen displays under the OPA icon all files with the specified extension in the path you have requested.)

ICON gives the name of the bitmap file to use as the icon for this OPA. If no file extension is given, .PIC is used. If you do not use ICON, the OPA is shown on the System screen with a standard OPA icon.

As mentioned above, you should add $1000 to the argument to TYPE for a Series 3a icon. This specifies that the icon has size 48x48 pixels (instead of 24x24 as it was on the Series 3). If the first bitmap has size 24x24, it is ignored and the following two bitmaps must be the 48x48 black and grey icons respectively. If the first bitmap is 48x48, it is assumed to be the black icon and the grey icon must follow. If $1000 is not set, a scaled up 24x24 icon will be used. The translator does not check the size of the icons. If you want to design your own icon using an OPL program, see gSAVEBIT for details on saving both black and grey planes to a bitmap file.

The arguments to any of the keywords between APP and ENDA must be constants and not expressions. So, for example, you must use "TYPE $1003" instead of "TYPE $1000 OR 3".

Running the OPA

Once you've translated the OPL file, return to the System screen and use Install on the App menu to install the OPA in the System screen. (You only need to do this once.) Once installed, file-based OPAs are shown with the list of available files, if any are found. Otherwise, the name used after the APP keyword appears below the icon.

(Note: the translated OPA is saved in a "\APP" directory. If you previously translated the module without the APP...ENDA at the start, the old translated version will still be listed under the RunOpl icon, and should be deleted.)

The first thing a file-based OPA should do is to get the name of the file to use, and check whether it is meant to create it or open it. "CMD$(2)" returns the full name of the file to use; "CMD$(3)" returns "C" for "Create" or "O" for "Open". All file-based OPAs (types 1 to 4) should handle both these cases; if a "Create" fails because the file exists already, or an "Open" fails because it does not, OPL raises the error, and the OPA should take suitable action - perhaps even just exiting.

How the SERIES 3a talks to an OPA

When the Series 3a wants an OPA to exit or to switch files, it sends it a System message, in the form of an event. This would happen if you press Delete to stop a running OPA, or select a new file for a type 2 or 3 OPA.

TESTEVENT and GETEVENT check for certain events, including both keypresses and System messages. All types of OPA must use these keywords to check for both keypresses and System messages; keyboard commands such as GET, KEY and KEYA cause other events to be discarded.

GETEVENT waits for an event whereas TESTEVENT simply checks whether an event has occurred without getting it.

If TESTEVENT returns non-zero, an event has occurred, and can be read with GETEVENT. This takes one argument, the name of an integer array for example, "GETEVENT a%()". The array should be at least 6 integers long. (This is to allow for future upgrades you only need use the first two integers.)

If the event is a keypress:
  a%(1)= keycode (as for GET)
  a%(2) AND $00ff= modifier (as for KMOD)
  a%(2)/256= auto-repeat count (ignored by GET; you can ignore it too)
For non-key events "(a%(1) AND $400)" will be non-zero. If the event is a System message to change files or quit, "a%(1)=$404". You should then use GETCMD$ to find the action required.

GETCMD$ returns a string, whose first character is "C", "O" or "X". If it is "C" or "O", the rest of the string is a filename.

You can only call GETCMD$ once for each event. You should do so as soon as possible after reading the event. Assign the value returned by GETCMD$ to a string variable so that you can extract its components.

If you have "c$=GETCMD$", the first character, which you can extract with "LEFT$(c$,1)", has the following meaning:

"C" close down the current file, and create the specified new file.

"O" close down the current file, and open the specified existing file.

"X" close down the current file (if any) and quit the OPA.

Again with "c$=GETCMD$", "MID$(c$,2,128)" is the easiest way to extract the filename.

Note: events are ignored while you are using keywords which pause the execution of the program GET, GET$, EDIT, INPUT, PAUSE, MENU and DIALOG. If you need to use these keywords, use "LOCK ON / LOCK OFF" (described later) around them to prevent the System screen from sending messages.

Example OPAs

Here is a type 0 OPA, which just prints the keys you press. The keyboard procedure "getk%:" returns the key pressed, as with GET, but jumps to a procedure "endit:" if a System message to close down is received. (Type 0 OPAs do not receive "change file" messages.)

"getk%:" does not return events with values 256 ($100) or above, as they are not simple keypresses. This includes the non-typing keys like Menu ($100-$1FF), hot-keys ($200-$3FF), and non-key events ($400 and above).

APP myapp0 TYPE $1000 ICON "\opd\me" ENDA

Here is a similar type 3 OPA. It does the same as the previous example, but System messages to change files cause the procedure "fset:" to be called. The relevant files are opened or created; the name of the file in use is shown in the status window.

APP myapp3 TYPE $1003 ICON "\opd\me" ENDA

You should, as in both these examples, be precise in checking for the System message; if in future the GETCMD$ function were to use values other than "C", "O" or "X", these procedures would ignore them.

If you need to check the modifier keys for the returned keypress, use "a%(2) AND $00ff" instead of KMOD.

SETNAME extracts the main part of the filename from any file specification (even one that is not DOS-like), in the same way as PARSE$. Using SETNAME ensures that the correct name will be used in the file list in the System screen. If an OPA lets you change files with its own `Open file' option, it should always use SETNAME to inform the System screen of the new file in use.

To be strict, whenever creating a file, an OPA should first use PARSE$ to find the disk and directory requested. It should then use "TRAP MKDIR" to ensure that the directory exists.

When an OPA cannot respond

The LOCK command marks an OPA as locked or unlocked. When an OPA is locked with "LOCK ON", the System will not send it events to change files or quit. If, for example, you move onto the file list in the System screen and press Delete to try to stop that running OPA, a message will appear, indicating that the OPA cannot close down at that moment.

You should use "LOCK ON" if your OPA uses a keyword, such as EDIT, which pauses the execution of the program. You might also use it when the OPA is about to go busy for a considerable length of time, or at any other point where a clean exit is not possible. Do not forget to use "LOCK OFF" as soon as possible afterwards.

An OPA is initially unlocked.

Designing an icon

As discussed earlier, an OPA icon is black and grey and has size 48 by 48 pixels. The icon is stored as two 48x48 bitmaps, black followed by grey, in a bitmap file. Here is a simple example program which creates a suitable bitmap:

Here the window is created with a grey plane (the sixth argument to gCREATE) gSAVEBIT automatically saves a window with both black and grey plane to a file in the required format.

In the OPA itself use the ICON keyword, as explained previously, to give the name of the bitmap file to use here, "ICON "\opd\me"".

OPAs and the status window

If you use "STATUSWIN ON,2" to display the status window, it shows the OPA's own icon and the name used with the APP keyword. "STATUSWIN ON,1" displays the smaller status window.

Important: The permanent status window is behind all other OPL windows. In order to see it, you must use FONT (or both SCREEN and gSETWIN) to reduce the size of the text and graphics windows. You should ensure that your program does not create windows over the top of it.

You can also display a list of modes/views for use with the DIAMOND key with DIAMINIT and position the DIAMOND indicator with "DIAMPOS".

The name can be changed with the SETNAME command. In general, an OPA should use SETNAME whenever it changes files, or creates a new file.

Other TYPE options

You can add any of these numbers to the value you use with TYPE:

For example, use "TYPE $8001" for a type 1 OPA having the first of the features above. (Note that "TYPE $8000+1" would fail to translate as the translator cannot evaluate expressions for any keywords between APP and ENDA).


Tricks

The calculator memories

The calculator memories M0 to M9 are available as floating-point variables in OPL. You might use them to allow OPL access to results from the calculator, particularly if you use OPL procedures from within the calculator.

It's best not to use them as a substitute for declaring your own variables. Your OPL program might go wrong if another running OPL program uses them, or if you use them yourself in the calculator.

Running a program twice

Although you may never need to, you can run more than one copy of the same translated OPL module at the same time. There are two ways:

Foreground and background

Each of these should be followed by "gUPDATE" to ensure they take effect immediately.

This example program comes to the foreground and beeps whenever you turn the Series 3a on. Be careful to enter the CALL and GETEVENT statements exactly as shown.

Note: when a program runs in the background it can stop the "automatic turn off" feature from working. However, as soon as the program waits for a keypress or an event, with GET/GET$ or GETEVENT, auto-turn off can occur.

Auto-turn off can also occur if the program does a PAUSE (of 2 or more 20ths of a second), but only if the program has used "CALL($138b)" ("unmark as active")


Cacheing procedures

Without procedure cacheing, procedures are loaded from file whenever they are called and discarded when they return LOADM simply loads a map listing procedure names and their positions in the module file so that they can be loaded fairly efficiently. The cache handling commands provide a method for keeping the code for a procedure loaded after it returns it remains loaded until a procedure called later requires the space in the cache. The strategy is then to remove the least recently used procedures, making it more likely that all the procedures called together in a loop, for example, remain in the cache together, thus speeding up procedure calling significantly.

Cache handling keywords allow you to:

Note : If you use hex, you can even exceed this figure, if you need to eg "CACHE $9000,$9000". However, you cannot exceed the 64k total memory limit which each Series 3a process has.

Cache size

Cacheing procedures is not a cure all. Care should be taken that the cache size is sufficient to load all procedures required for a fast loop otherwise, for example, a large procedure may cause all the small ones in a loop to be removed and equally, a small one may require the large one to be removed, so that the cache provides no benefit at all. In fact, the overhead needed for cache management can then make your program less efficient than having no cache at all. If the maximum cache size you can have is limited, careful use of "CACHE OFF" should prevent such problems at the expense of not fitting all the procedures in the loop in the cache. "CACHE OFF" is implemented very efficiently and calling it frequently in a loop should not cause much concern.

To guarantee that there is enough memory for a given cache size, create the cache passing that value as the initial size using "TRAP CACHE init%,max%". TRAP ensures that if the cache creation succeeds, ERR returns zero and otherwise the negative `Out of memory' error is raised. After creation, the cache will grow as required up to the maximum size "max%" or until there is not enough free memory to grow it. On failure to grow the cache, any procedures which will not fit into the existing cache, even when unused procedures are removed, are simply loaded without using the cache and are discarded when they return.

If you want to ensure a certain minimum cache size, say 10000 bytes, but do not care how large it grows, you could use "TRAP CACHE 10000,$ffff" so that the cache just grows up to the limits of memory. For a relatively small program, you might want to load the whole program into cache by making the cache size the same size as the module. This will in fact be a little larger than required, unnecessarily including a procedure name table and module file header which are not loaded into the cache. The minimum cache size is 2000 bytes, which is used if any lower value is specified. If the maximum size specified is less than the initial size, the maximum is set to the initial size. The maximum cache size cannot be changed once the cache has been created and an error is returned if you attempt to do so.

Note : The initial cache size should ideally be large enough to hold all procedures that are to be cached simultaneously. There is no advantage in growing the cache from its initial size when you know that a certain minimum size is needed.

Procedures in unloaded modules

When a module is unloaded, all procedures in it that are no longer in use are removed from the cache. Any procedure that is still in use, is hidden in the cache by changing its first character to lower case; when it finally returns, a hidden procedure is removed in the normal manner to make room for loading a new procedure when the cache is full. Note that it considered bad practice to unload a module containing procedures that are still running eg. for a procedure to unload its own module.

Cache timings

Calling an empty procedure that simply returns is approximately 10 times faster with a cache. This figure was obtained by calling such a procedure 10000 times in a loop, both with cacheing off and on, and subtracting the time taken running an empty loop in each case.

Clearly that case is one of the best for showing off the advantages of cacheing and there is no general formula for calculating the speed gain. The procedures that benefit most will be those that need most module file access relative to their size in order to load them into memory. The programmer cannot reasonably write code taking this into account, so no further details are provided here.

The case described above does not require any procedures to be removed from the cache to make room for new procedures when the cache is full, and removal of procedures requires a fair amount of processing by the cache manager. If many procedures in a time-critical section of your program are loaded into the cache and not used often before removal, the speed gain may be less than expected a larger cache may be called for to prevent too many removals.

It should be noted however, that even with the worst case of procedures being loaded into the cache for use just once before removal, having a cache is often superior to having no cache. This is because the cache manager reads module file data (required for loading the procedures into memory) in one block rather than a few bytes at a time and it is the avoidance of excessive file access which provides the primary speed gains for cacheing.

Compatibility mode modules

Procedures in modules translated for the Series 3 cannot be loaded into the cache. On encountering such a procedure, the cache manager simply loads it without using the cache and discards it when it returns. The reason for this is that a few extra bytes of data are stored in the Series 3a modules which are needed by the cache manager.

Potential problems in existing programs

It is possible that previously undiscovered bugs in existing OPL programs are brought to light simply by adding code to use the cache.

Without cacheing, the variables in a procedure are followed immediately by the code for the procedure. Writing beyond the variables (for example reading too many bytes into the final variable using such keywords as gPEEKLINE or KEYA) would have written over the code itself but would have gone unnoticed unless you happened to loop back to the corrupted code. With a cached procedure, the code no longer follows your variables, so the corruption occurs elsewhere in memory, resulting quite probably in the program crashing.

Controlling procedure cacheing

"TRAP CACHE initSize%,maxSize%" creates a cache of a specified initial number of bytes, which may grow up to the specified maximum. If the maximum is less than the initial size, the initial size becomes the maximum. If growing the cache fails, normal loading without the cache is used. The `In use' error (-9) is raised if a cache has been created previously or the `Out of memory' error (-10) on failure to create a cache of the specified initial size use the TRAP command if required. Procedure code and other information needed for setting up variables are loaded into the cache when the procedure is called. If there is no space in the cache and enough space can be regained, the least recently used procedures are removed. Otherwise the procedure is loaded in the normal way without cacheing.

Once a cache has been created, "CACHE OFF" prevents further cacheing although the cache is still searched when calling subsequent procedures. "CACHE ON" may then be used to reenable cacheing. Note that "CACHE ON" or "CACHE OFF" are ignored if used before "CACHE initSize%,maxSize%".

Tidying the cache

CACHETIDY removes any procedures from the cache that have returned to their callers. This might be called after performing a large, self-contained action in the program which required many procedures. Using CACHETIDY will then result in speedier searching for procedures called subsequently and, more importantly, will prevent the procedures being unloaded one at a time when the need arises it is very efficient to remove a set of procedures that are contiguous in the cache as is likely to be the case in this situation.

Note that a procedure which has returned is automatically removed from the cache if you unload the module it is in, so CACHETIDY needn't be used for such a procedure.

Getting cache index header information

The CACHEHDR command is provided for advanced use and is intended for use during program development only.

"CACHEHDR ADDR(hdr%())" reads the current cache index header into the array "hdr%()" which must have at least 11 integer elements. Note that any information returned is liable to change whenever a procedure is called, so you cannot save these values over a procedure call.

If no cache has yet been created, "hdr%(10)=0" and the other data read is meaningless. Otherwise, the data read is as follows:

hdr%(1)		current address of the cache itself
hdr%(2)		number of procedures currently cached
hdr%(3)		maximum size of the cache in bytes
hdr%(4)		current size of the cache in bytes
hdr%(5)		number of free bytes in the cache
hdr%(6)		total number of bytes in cached procedures which are
		freeable (ie. not running)
hdr%(7)		offset from the start of the cache index to the first 
		free index record
hdr%(8)		offset from start of cache index to most recently used
		procedure's record; zero if none
hdr%(9)		offset from start of cache index to least recently used
		procedure's record; zero if none
hdr%(10)	address of the cache index, or zero if no cache created yet
hdr%(11)	non-zero if cacheing is on, and zero if it is off
The cache manager maintains an index for the cache consisting of an index header containing overall information for the whole cache as well as one index record for each procedure cached. All offsets mentioned above give the number of bytes from the start of the index to the procedure record specified. The index records for cached procedures form a doubly linked list, with one list beginning with the most recently used procedure (MRU), with offset given by "hdr%(8)", and the other with the least recently used procedure (LRU) with offset given by "hdr%(9)". A further singly linked list gives the offsets to free index records. The linkage mechanism is described in the discussion of CACHEREC below.

Getting a cache index record

The CACHEREC command is provided for advanced use and is intended for use during program development only.

"CACHEREC ADDR(rec%()),offset%" reads the cache index record (see the description of CACHEHDR above) at "offset%" into array "rec%()" which must have at least 18 integer elements. "offset%=0" specifies the most recently used (MRU) procedure's record if any and "offset%<0" the least recently used procedure (LRU) procedure's record if any.

The data returned by CACHEREC is meaningless if no cache exists (in which case "rec%(17)=0") or if there are no procedures cached yet (when "hdr%(8)=0" as returned by CACHEHDR).

Each record gives the offset to both the more recently used and to the less recently used procedure's record in the linked lists, except for the MRU and the LRU procedures' records themselves which each terminate one of the lists with a zero offset. The first free index record (see CACHEHDR above) starts the free record list, in which each record gives the offset of the next free record or zero offset to terminate the list. To "walk" the cache index, you would always start by calling CACHEREC specifying either the MRU or LRU record offset, and use the values returned to read the less or more recently used procedure's record respectively. Note that any information returned is liable to change whenever a procedure is called, so you cannot save these values over a procedure call.

For the free cell list, only "rec%(1)" is significant, giving the offset of the next free index record. For the records in the lists starting with either the LRU or MRU record, the data returned in "rec%()" is:

rec%(1) offset to less recently used procedure's record or zero if on LRU rec%(2) offset to more recently used procedure's record or zero if on MRU rec%(3) usage count zero if not running rec%(4) offset in cache itself to descriptor for building the procedure frame rec%(5) offset in cache itself to translated code for the procedure rec%(6) offset in cache itself to the end of the translated code for the procedure rec%(7) number of bytes used by the procedure in the cache itself rec%(8-15) leading byte counted procedure name, followed by some private data rec%(16) address of the procedure's leading byte counted module name rec%(17) address of the cache index, or zero if no cache created yet rec%(18) non-zero if cacheing is on, and zero if it is off

For example, to print the names of procedures and their sizes from MRU to LRU:

CACHEHDR ADDR(hdr%())
IF hdr%(10)=0
  PRINT "No cache created yet"
  RETURN
ENDIF
IF hdr%(8)=0                    rem MRU zero?
  PRINT "None cached currently"
  RETURN
ENDIF
rec%(1)=0                       rem MRU first
DO
  CACHEREC ADDR(rec%()),rec%(1) rem less recently used
											rem proc
  PRINT PEEK$(ADDR(rec%(8))),rec%(7) rem name and size
UNTIL rec%(1)=0


Sprite handling

How sprites work

OPL includes a set of keywords for handling a sprite a user-defined black/grey/white graphics object of variable size, displayed on the screen at a specified position.

The sprite can also be animated you can specify up to 13 bitmap-sets which are automatically presented in a cycle, with the duration for each bitmap-set specified by you. Each bitmap-set may be displayed at a specifiable offset from the sprite's notional position.

The 13 bitmap-sets are each composed of up to six bitmaps. The set pixels in each bitmap specify one of the following six actions: black pixels to be drawn; black pixels to be cleared; black pixels to be inverted; grey pixels to be drawn; grey pixels to be cleared; or grey pixels to be inverted. The bitmaps in a set must have the same size.

All the bitmaps in a set are drawn to the screen together and displayed for the specified duration, followed by the next set, and so on.

If you do not specify that a pixel is to be drawn, cleared or inverted, the background pixel is left unchanged.

Black pixels are drawn "on top of" grey pixels, so if you clear/invert just the grey pixels in the sprite they will be hidden under any pixels set black. So to clear/invert pixels on a background which has both grey and black pixels set, you need to clear/invert both black and grey pixels in the sprite.

Note : The pixels of one colour (black or grey) which are set in one bitmap of the bitmap-set should not overlap with those of the same colour which are set in another bitmap in the same bitmap-set. This is because the order in which the bitmaps are applied is undefined. So, for example, do not specify that pixel (0,0) should have the black pixel both drawn and cleared.

Why use sprites?

A sprite is useful for displaying something in foreground without having to worry about restoring the background display. Also a sprite can have any shape, leaving the background display all around it intact, and it can even be hollow only the pixels specified by you are drawn, cleared or inverted. Typically only one bitmap-set containing two black bitmaps would be used one for setting and one for clearing pixels.

You would not often use the sprite features in their full generality. In fact, more than one bitmap-set is needed only for animation and it is also seldom necessary to use all the available bitmaps in a single bitmap-set.

Creating a sprite

sprId%=CREATESPRITE creates a sprite and returns the sprite ID.

Appending a bitmap-set to a sprite

APPENDSPRITE tenths%,bitmap$()
APPENDSPRITE tenths%,bitmap$(),dx%,dy%
append a single bitmap-set to a sprite. These may be called up to 13 times for each sprite. "APPENDSPRITE" may be called only before the sprite is drawn, otherwise it raises an error. tenths% gives the duration in tenths of seconds for the bitmap-set to be displayed before going on to the next bitmap-set in the sequence. It is ignored if there is only one bitmap-set.

bitmap$() contains the names of the six bitmap files in the set:

bitmap$(1) for setting black pixels
bitmap$(2) for clearing black pixels
bitmap$(3) for inverting black pixels
bitmap$(4) for setting grey pixels
bitmap$(5) for clearing grey pixels
bitmap$(6) for inverting grey pixels
Use "" to specify no bitmap. If "" is used for all the bitmaps in the set, the sprite is left blank for the specified duration.

The array must have at least 6 elements.

All the bitmaps in a single bitmap-set must be the same size, otherwise an `Invalid arguments' error is raised on attempting to draw the sprite. Bitmaps in different bitmap-sets may differ in size. dx% and dy% are the (x,y) offsets from the sprite position (see CREATESPRITE) to the top-left of the bitmap-set with positive for right and down. The default value of each is zero.

Sprites may use considerable amounts of memory. A sprite should generally be created, initialised and closed in the same procedure to prevent memory fragmentation. Care should also be taken in error handling to close a sprite that is no longer in use.

Creating or changing a sprite consisting of many bitmaps requires a lot of file access and should therefore be avoided if very fast sprite creation is required. Once the sprite has been drawn, no further file access is performed (even when it is animated) so the number of bitmaps is no longer important.

Drawing a sprite

DRAWSPRITE x%,y% draws a sprite in the current window with top-left at pixel position (x%,y%). The sprite must previously have been initialised using APPENDSPRITE or the `Resource not open' error (-15) is raised. If any bitmap-set contains bitmaps with different sizes, DRAWSPRITE raises an `Invalid arguments' error (-2).

Changing a bitmap-set in a sprite

CHANGESPRITE index%,tenths%,var bitmap$()
CHANGESPRITE index%,tenths%,var bitmap$(),dx%,dy%
change the bitmap-set specified by "index%" (1 for the first bitmap-set) in the sprite using the supplied bitmap files, offsets and duration which are all used in the same way as for APPENDSPRITE.

CHANGESPRITE can be called only after DRAWSPRITE.

Note that if all or many bitmap-sets in the sprite need changing or if each bitmap-set consists of many bitmaps, the time required to read the bitmaps from file may be considerable, especially if fast animation is in progress. In such circumstances, you should think about closing the sprite and creating a new one, which will often be more efficient.

Positioning a sprite

POSSPRITE x%,y% sets the position of the sprite to (x%,y%).

Closing a sprite

CLOSESPRITE sprId% closes the sprite with ID sprId%.

Sprite example

The following code illustrates all the sprite handling keywords using a sprite consisting of just two bitmap-sets each containing a single bitmap.


Scanning the keyboard directly

It is sometimes useful to know which keys are being pressed at a given moment and also when a key is released. For example, in a game, a certain key might start some action and releasing the key might stop it.

CALL($288e,ADDR(scan%())) returns with the array scan%(), which must have at least 10 elements, containing a bit set for keys currently being pressed.

Every key on the keyboard is represented by a unique bit. This includes the modifier keys (Shift, Control etc) and the application buttons (System, Data, Word etc).

A set bit simply signifies a pressed key a key pressed on its own gives one bit set; that same key with a modifier gives the same bit set with another bit for the modifier; the modifier on its own gives the same modifier bit on its own.

The following table lists each key (according to the text printed on the physical key itself), the scan%() array element for that key and the hexadecimal bit mask to be "AND"ed with that array element to check whether the key is being pressed.

For example, pressing Tab sets bit 2 of scan%(1), pressing Control sets bit 7 of scan%(3) and pressing both together sets both these bits. So Tab is being pressed if scan%(1) AND $04 is non-zero, and Control is being pressed if scan%(3) AND $80 is non-zero.

A possible strategy for scanning the keys might be to wait for any key of interest using "GETEVENT" or "GET" (allowing switch off and less intensive use of the battery); start the required action which is to be continued only while the key is being pressed; scan the keyboard as discussed above until the key is released and then stop the action; wait for the next key and repeat.

Note : Note that the key returned by "GETEVENT" or "GET" is not precisely synchronised with those scanned, so once you have waited for a relevant key you should scan for all the keys pressed, ignoring the keycode returned by "GETEVENT" or "GET".


I/O functions and commands

OPL includes powerful facilities to handle input and output (`I/O'). These functions and commands can be used to access all types of files on the Series 3a, as well as various other parts of the low-level software.

This section describes how to open, close, read and write to files, and how to set the position in a file. The data file handling commands and functions have been designed for use specifically with data files. The I/O functions and commands are designed for general file access. You don't need to use them to handle data files.

These are powerful functions and commands and they must be used with care. Before using them you must read this chapter closely and have a good grounding in OPL in general.

Error handling

You should have a good understanding of error handling before using the I/O functions.

The functions in this section never raise an OPL error message. Instead they return a value if this is less than zero an error has occurred. It is the responsibility of the programmer to check all return values, and handle errors appropriately. Any error number returned will be one of those in the list given in the error handling chapter. You can use ERR$ to display the error as usual.

Handles

Many of these functions use a handle, which must be an integer variable. IOOPEN assigns this handle variable a value, which subsequent I/O functions use to access that particular file. Each file you IOOPEN needs a different handle variable.

`var' variables

In this section, `var' denotes an argument which should normally be a LOCAL or GLOBAL variable. (Single elements of arrays may also be used, but not field variables or procedure parameters.) Where you see `var' the address of the variable is passed, not the value in it. (This happens automatically; don't use ADDR yourself.)

In many cases the function you are calling passes information back by setting these `var' variables.

`var' is just to show you where you must use a suitable variable you don't actually type it.

For example:
ret%=IOOPEN(var handle%,name$,mode%)
indicates that "IOOPEN(h%,"abc",0)" is OK while" "IOOPEN(100,"abc",0) is incorrect.

It is possible, though, that you already have the address of the variable to use. It might be that this address is held in a field variable, or is even a constant value, but the most common situation is when the address was passed as a parameter to the current procedure.

If you add a `#' prefix to a `var' argument, this tells OPL that the expression following is the address to be used, not a variable whose address is to be taken.

Here is an example program:

The current value held in phandle% is passed to IOOPEN. You might call doopen: like this:

local filhand%,...
...
doopen:(addr(filhand%), "log.txt", $23)
...
The "doopen:" procedure calls IOOPEN with the address of "filhand%", and IOOPEN will write the handle into "filhand%".

Note : If you ever need to add or subtract numbers from the address of a variable, use the UADD and USUB functions, or you run the risk of `Integer overflow' errors.

Opening a file with IOOPEN

ret%=IOOPEN(var handle%,name$,mode%)

or

ret%=IOOPEN(var handle%,address%,mode%)

for unique file creation

Creates or opens a file (or device) called name$ and sets handle% to the handle to be used by the other I/O functions.

mode% specifies how the file is to be opened. It is formed by ORing together values which fall into the three following categories:

Mode Category 1 Open mode
One and only one of the following values must be chosen from this category.

$0000

Open an existing file (or device). The initial current position is set to the start of the file.

$0001

Create a file which must not already exist.

$0002

Replace a file (truncate it to zero length) or create it if it does not exist.

$0003

Open an existing file for appending. The initial current position is set to the end of the file. For text format files (see $0020 below) this is the only way to position to end of file.

$0004

Creates a file with a unique name. For this case, you must use the address of a string instead of "name$". This string specifies only the path of the file to be created (any file name in the string is ignored). The string at "address%" is then set by IOOPEN to the unique file name generated (this will include the full path). The string must be large enough to take 130 characters (the maximum length file specification). For example: " s$="M:\home\" IOOPEN(handle%,ADDR(s$),mode%) "This mode is typically used for temporary files which will later be deleted or renamed."

Mode Category 2 File format
One and only one of the following values must be chosen from this category. When creating a file, this value specifies the format of the new file. When opening an existing file, make sure you use the format with which it was created.

$0000

The file is treated as a byte stream of binary data with no restriction on the value of any byte and no structure imposed upon the data. Up to 16K can be read from or written to the file in a single operation.

$0020

The file is treated as a sequence of variable length records. The records are assumed to contain text terminated by any combination of the CR and LF ("$0D", "$0A") characters. The maximum record length is 256 bytes and Control-Z ("$1A") marks the end of the file.

Mode Category 3 Access flags
Any combination of the following values may be chosen from this category.

$0100

Update flag. Allows the file to be written to as well as read. If not set, the file is opened for reading only. You must use this flag when creating or replacing a file.

$0200

Choose this value if you want the file to be open for random access (not sequential access), using the IOSEEK function.

$0400

Specifies that the file is being opened for sharing for example, with other running programs. Use if you want to read, not write to the file. If the file is opened for writing ("$0100" above), this flag is ignored, since sharing is then not feasible. If not specified, the file is locked and may only be used by this running program.

Closing a file with IOCLOSE

Files should be closed when no longer being accessed. This releases memory and other resources back to the system.

ret%=IOCLOSE(handle%)

Closes a file (or device) with the handle "handle%" as set by IOOPEN.

Reading a file with

IOREAD

ret%=IOREAD(handle%,address%,maxLen%)

Reads up to "maxLen%" bytes from a file with the handle "handle%" as set by IOOPEN. "address%" is the address of a buffer into which the data is read. This buffer must be large enough to hold a maximum of "maxLen%" bytes. The buffer could be an array or even a single integer as required. No more than 16K bytes can be read at a time.

The value returned to "ret%" is the actual number of bytes read or, if negative, is an error value.

Text files
If "maxLen%" exceeds the current record length, data only up to the end of the record is read into the buffer; no error is returned and the file position is set to the next record.

If a record is longer than "maxLen%", the error value `Record too large' (-43) is returned. In this case the data read is valid but is truncated to length "maxLen%", and the file position is set to the next record.

A string array "buffer$(255)" could be used, but make sure that you pass the address "UADD(ADDR(buffer$),1)" to IOREAD. This leaves the leading byte free. You can then POKEB the leading byte with the count (returned to "ret%") so that the string conforms to normal string format. See the example program.

Binary files
If you request more bytes than are left in the file, the number of bytes actually read (even zero) will be less than the number requested. So if "ret% To read up to 16K bytes (8192 integers), you could declare an integer array "buffer%(8192)".

Writing to a file

ret%=IOWRITE(handle%,address%,length%)

Writes "length%" bytes stored in a buffer at "address%" to a file with the handle "handle%".

When a file is opened as a binary file, the data written by IOWRITE overwrites data at the current position.

When a file is opened as a text file, IOWRITE writes a single record; the closing CR/LF is automatically added.

Positioning within a file

ret%=IOSEEK(handle%,mode%,var offset&)

Seeks to a position in a file that has been opened for random access (see IOOPEN above).

"mode%" specifies how the argument "offset&" is to be used. "offset&" may be positive to move forwards or negative to move backwards. The values you can use for "mode%" are:

1    	Set position in a binary file to the absolute value specified
	in "offset&", with 0 for the first byte in the file.
2   	Set position in a binary file to "offset&" bytes from the 
	end of the file.
3   	Set position in a binary file to "offset&" bytes relative 
	to the current position.
6    	Rewind a text file to the first record. "offset&" is not 
	used, but you must still pass it as a argument, for compatibility
	with the other cases.
IOSEEK sets the variable "offset&" to the absolute position set.

Example - displaying a plain text file

This program opens a plain text file, such as one created with the `Save as' option in the Word Processor or Database, and types it to the screen. Press Esc to quit and any other key to pause the typing to the screen.

I/O device handling

The following I/O functions provide access to devices. A full description is not within the scope of this manual, since these functions require extensive knowledge of the Series 3a operating system and related programming techniques. The syntax and argument descriptions are provided here for completeness.

ret%=IOW(handle%,func%,var arg1,var arg2) - The device driver opened with handle% (as returned by IOOPEN) performs the synchronous I/O function func% with the two further arguments. The size and structure of these two arguments is specified by the particular device driver's documentation.

ret%=IOA(handle%,func%,var status%,var arg1,var arg2) - The device driver opened with handle% (as returned by IOOPEN) performs the asynchronous I/O function func% with two further arguments. The size and structure of these two arguments is specified by the particular device driver's documentation.

Asynchronous means that the IOA returns immediately, and the OPL program can carry on with other statements. status% will usually be set to -46, which means that the function is still pending.

When, at some later time, the function completes, status% is automatically changed. (For this reason, "status%" should usually be global if the program is still running, "status%" must be available when the request completes, or the program will probably crash.) If "status%" >=0, the function completed without error. If <0, the function completed with error. The error number is specific to the device driver.

At the same time, a signal is sent to the running OPL program.

In most cases, you cannot perform another I/O read/write function to this device until you first read the signal of this function's completion. If this is the only I/O device with a function pending, wait for the signal with "IOWAITSTAT status%". (If you have other functions pending on other devices, you must use IOWAIT and IOSIGNAL. These commands are described below.)

Alternatively, you can cancel the pending function with "IOW(handle%,4)". The program will still receive a signal, which should be read with IOWAITSTAT or IOWAIT.

If an OPL program is ready to exit, it does not have to wait for any signals from pending IOA calls.

IOWAIT Wait for an asynchronous request (such as one requested by IOC or IOA) to complete. IOWAIT returns when any asynchronous I/O function completes. Check "status%" to see whether it was the function which you called with IOA. You must keep a count of the number of times IOWAIT returns due to other functions completing. When "status%" finally shows that IOWAIT has returned because of this function completing, you must then call IOSIGNAL once for each other function which completed, to replace these other signals.

If you have no other functions pending on different I/O handles, use IOWAITSTAT instead.

IOSIGNAL Replace a signal of an I/O function's completion. See IOWAIT.

IOWAITSTAT var status% Wait for a particular asynchronous function, called with IOA, to complete. This saves using IOWAIT, checking each time to see if it was the desired function completing, and finally calling IOSIGNAL for each unexpected function completion.

"IOYIELD"" ensure that any asynchronous function is given a chance to run. Some devices are unable to perform an asynchronous request if an OPL program becomes computationally intensive, using no I/O (screen, keyboard etc) at all. In such cases, the OPL program should use IOYIELD before checking its "status%" variable. IOYIELD is the equivalent of IOSIGNAL followed by IOWAIT the IOWAIT returns immediately with the signal from IOSIGNAL, but the IOWAIT causes any asynchronous handlers to run.

IOC(handle%,func%,var stat%,var a1,var a2)
IOC(handle%,func%,var stat%,var a1)
IOC(handle%,func%,var stat%) - Make an I/O request with guaranteed completion. This has the same form as IOA but it returns zero always (ie the return value can be ignored). It is effectively the same as:

ret%=IOA(h%,f%,stat%,...)
IF ret%<0
  IF ret%=-46 :RAISE -1 :ENDIF
  stat%=ret% :IOSIGNAL
ENDIF
IOC allows you to assume that the request started successfully any error is always given in the status word "stat%". If there was an error, "stat%" contains the error code and the IOSIGNAL causes the next IOWAIT to return immediately as if the error occurred after completion. There is seldom a requirement to know whether an error occurred on starting a function, and IOC should therefore be used in preference to IOA nearly always.

IOCANCEL(handle%) - Cancels any outstanding asynchronous I/O request (IOC or IOA) on the specified channel, causing them to complete with the completion status word containing -48 ("I/O cancelled"). The return value is always zero and may be ignored. Device drivers that support truly asynchronous services provide a cancel service. The detailed effect of the cancel depends on the device driver. However, the following general principles apply:

The IOCANCEL function is harmless if no request is outstanding (eg if the function completed just before cancellation requested).

err%=KEYA(var status%,key%(1)) - This is an asynchronous keyboard read function. You must declare an integer array with two elements here, key%(1) and key%(2) to receive the keypress information. If a key is pressed, the information is returned in this way:

KEYA needs an IOWAIT in the same way as IOA.

KEYA has been included in OPL because the handle of the keyboard driver is unknown to the programmer. Otherwise it is equivalent to IOA(keyhand%,1,status%,key%()).

"err%=KEYC(var status%)"" Cancels a KEYA.

Some useful IOW functions

IOW has this specification:

ret%=IOW(handle%,func%,var arg1,var arg2)

Here are some uses:

LOCAL a%(6)
  a%(1)=x1% :a%(2)=y1%
  a%(3)=x2% :a%(4)=y2%
  IOW(-2,8,a%(),a%()) REM 2nd a% is ignored
reads the cursor position in the rectangle x1%,y1% (top left), x2%,y2% (bottom right), writing the x and y positions to a%(5) and a%(6) respectively. This returns 0,0, not 1,1, as the top left.

Set "x1%,y1%,x2%,y2% to the screen top left and bottom right (set by SCREEN), to read the cursor position in the current screen.

LOCAL i%,a%(6)
  i%=2
  a%(1)=x1% :a%(2)=y1%
  a%(3)=x2% :a%(4)=y2%
  IOW(-2,7,i%,a%())
clears a rectangle at x1%,y1% (top left), x2%,y2% (bottom right). If y2% is one greater than y1%, this will clear part or all of a line.

Example of IOW screen functions

The final two procedures in this module call the two IOW screen functions described beforehand. The rest of the module lets you select the function and values to use. It uses the technique used in the `Friendlier interaction' chapter of handling menus and hot-keys by calling procedures with string expressions.

Alarm example IOC to ALM:

The "ALM:" device provides access to alarms. When writing to it with IOW, IOC or IOA, you can use these two functions:

In either case, you must pass these two arguments:

This procedure asks for the information for an alarm, and sets it (as type 2 day and time to be shown when the alarm rings). If you press the Time button, and this is the next alarm to ring, it is shown as a `RunOpl' alarm.

At the moment the alarm rings, either "s%" must still be available to take the status word set by this ALM: function, or the program must have exited. Otherwise the status word will be written to a random area of memory. So in this example, no error-checking is done after the IOC - the program just ends. So you would have to use this as a whole program itself you must not call this procedure as written here from another procedure.

Dialling example IOW to SND:

The "SND:" device provides sound services on the Series 3a. One function, number 10, provides access to DTMF dialling. It requires these two arguments:


Recording and playing sounds

This section explains how to write a program which records sounds to a sound file using the in-built Series 3a microphone, and which plays these or pre-recorded sound files back.

Sound file structure

Series 3a sound files are files with a .WVE extension that contain a 32-byte header and a byte stream of digital sound which is sampled and played back at 8000 bytes per second (12-bit sound is converted to 8-bit using A-Law encoding).

The file header has the following format:

offset in file	bytes	contents
0		16	zero-terminated 'ALawSoundFile**'
16		2	version of this format
18		4	number of 8-bit samples
22		2	trailing silence in ticks
24		2	repeats
26		6	spare bytes reserved for future use
The number of samples is the number of bytes following the header and should always be size of file less 32 for the header.

The silence in ticks is the number of system ticks of silence appended to each repeat on playback (in practice, you get at least 2 ticks between repeats). A system tick is 1/32 seconds or 250 samples.

The repeats are the number of times to repeat the sound on playback (0 and 1 are treated as the same).

You can truncate the sound file, change the number of repeats and the trailing silence by changing the file length and header using the I/O binary file handling functions (IOOPEN, IOW, IOSEEK, IOWRITE etc) described elsewhere in this chapter.

For example, you can truncate the file to length newLen& using: ret%=IOW(handle%,11,newLen&,#0)

How to record and play sounds

The following set of procedures perform asynchronous recording and playing of sounds.

"recorda:(pstat%,inname$,size%)" and "recordw%:(inname$,size%)" respectively perform asynchronous and synchronous recording to file "inname$". Any existing file is replaced. You can only record to the Internal disk or to a RAM SSD you cannot record to a Flash SSD. (You can playback from a Flash SSD, however.)

"size%" specifies the maximum number of bytes to be recorded in units of 2048 bytes. To record for one second "size%=4". This figure excludes the 32-byte header. Before recording, a file of length "32+size%*2048" bytes is created and there must actually be room on the disk for a file of that length.

"pstat%" is the address of the status word to take the completion code for asynchronous recording.

"recordc:" cancels recording and truncates the file to the actual length recorded before cancellation.

"playa:(pstat%,inname$,ticks%,vol%) " and "playw%:(inname$,ticks%,vol%)" respectively perform asynchronous and synchronous playing of "inname$".

"inname$" should either be the filename of the sound file to play or a `*' followed by just the name component of the sound file. If it is preceded by a `*', the extension .WVE is assumed and the service automatically searches ROM:: and the \WVE directories of M: (Internal disk), A: and B: (in that order). The ROM:: .WVE files have names SYS$AL01, SYS$AL02 and SYS$AL03.

"ticks%" is the duration that the sound file will play back in system ticks. If it is shorter than the given sound then playback is truncated to that time. If "ticks%" is negative, in addition to truncating the playback of longer files, it pads out as necessary to that duration with silence. If duration is zero, it plays the file without truncation or padding. (Alarms use a parameter of -480 to truncate or pad out to 15 seconds.)

"volume%" is a number between 0 and 5 inclusive, with 0 being the loudest. On the Series 3a there are only 4 actual levels: 0/1, 2, 3, and 4/5. "pstat%" is the address of the status word to take the completion code for asynchronous playback. Playback will append periods of silence and repeat the sound as specified in the file header.

"playc:" cancels playing back a sound.

Alarms

The system dialogs that are used to set alarms detect the presence of any .WVE files in the \WVE directory of any local directory (in practice, A:, B: and Internal) and make these available (by file name) as the sound of the alarm.

When an alarm with a .WVE file rings, .WVE file playback (including any repeats) is clipped to 15 seconds. If the .WVE file plays for less than 15 seconds (including any repeats), the 15 seconds is padded out with silence.

Example of recording

The following asynchronously records "time%" seconds of sound to file "file$" or cancels the recording when any key is pressed. The section I/O device handling and asynchronous requests also in this chapter discusses the principles involved. The "recorda:" and "recordc:" procedures, from above, are used.


OPL database information

ODBINFO var info%() is provided for advanced use only and allows you to use OS and CALL to call DbfManager interrupt functions not accessible with other OPL keywords.

The description given here will be meaningful only to those who have access to full SDK documentation of the DbfManager services, which explains any new terms. Since that documentation is essential for use of "ODBINFO", no attempt is made here to explain these terms.

ODBINFO returns "info%()", which must have four elements containing pointers to four blocks of data; the first corresponds to the file with logical name "A", the second to "B" and so on.

Take extreme care not to corrupt these blocks of memory, as they are the actual data structures used by the OPL runtime interpreter.

A data block which has no open file using it has zero in the first two bytes. Otherwise, the block of data for each file has the following structure, giving the offset to each component from the start of the block and with offset 0 for the 1st byte of the block:

Offset	Bytes	Description
0	2	DBF system's file control block (handle)
		or zero if file not open
2	2	offset in the record buffer to the current record
4	2	pointer to the field name buffer
6	2	number of fields
8	2	pointer to start of record buffer
10	2	length of a NULL record
12	1	non-zero if all fields are text
13	1	non-zero for read-only file
14	1	non-zero if record has been copied down
15	1	number of text fields
16	2	pointer to device name


Example

To copy the Descriptive Record of logical file "B" to logical file "C":


DYL handling

This section contains a complete reference description of OPL's support for accessing previously created dynamic libraries (DYLs). These libraries have an object-oriented programming (OOP) user-interface and several have been built into the Series 3a ROM for use by the ROM applications. DYLs cannot be created using OPL.

Since a vast amount of documentation would need to be provided to describe the essential concepts of OOP and the services available in existing DYLs, no attempt is made to supply it here. This section simply introduces the syntax for all the OOP keywords supported in OPL with a brief description of each. Also, OOP terminology is used here without explanation, to cater for those who have previous experience of DYL handling in the `C' programming language.

`var' arguments

The use of `var' and "#" for arguments was discussed earlier in this chapter in the section `I/O functions and commands'. The DYL handling keywords use `var' and "#" in the same way, for example: "ret%=SEND(pobj%,method%,var p1,var p2,var p3)".

This is because many DYL methods need the address of a variable or of a structure to be passed to them.

When you use a LOCAL or GLOBAL variable as the `var' argument, the address of the variable is used. (You cannot use procedure parameters or field variables, for this reason.) If you use a "#" before a `var' argument, though, the argument/value is used directly, instead of its address being used.

If, for example, you need to call a method with "p1" the address of a long variable "a&", "p2" the integer constant 3, and "p3" the address of a zero terminated string "X", you could call it as follows:

s$="X"+CHR$(0) REM zero terminate p%=UADD(ADDR(s$),1) REM skip leading count byte ret%=SEND(pobj%,method%,a&,#3,#p%)

The address of "a&" is passed because there is no "#". "3" and the value in "p%" are passed directly (no address is taken) because they are preceded by "#".

Loading a DYL

"ret%=LOADLIB(var cathand%,name$,link%)" loads and optionally links a DYL that is not in the ROM. If successful, writes the category handle to "cathand%" and returns zero. You would normally only set "link%" to zero if the DYL uses another DYL which you have yet to load in which case LINKLIB would subsequently be used. The DYL is shared in memory if already loaded by another process.

Unloading a DYL

"ret%=UNLOADLIB(cathand%)" unloads a DYL from memory. Returns zero if successful.

Linking a DYL

"LINKLIB cathand%" links any libraries that have been loaded using LOADLIB. LINKLIB is not likely to be used much in OPL pass "link%" with a non-zero value to LOADLIB instead.

Finding a category handle given its name

"ret%=FINDLIB(var cathand%,name$)" finds DYL category "name$" (including ".DYL" extension) in the ROM. On success returns zero and writes the category handle to "cathand%". To get the handle of a RAM-based DYL, use LOADLIB which guarantees that the DYL remains loaded in RAM. FINDLIB will get the handle of a RAM-based DYL but does not keep it in RAM.

Converting a category number to a handle

"cathand%=GETLIBH(catnum%)" converts a category number "catnum%" to a handle. If "catnum%" is zero, this gets the handle for OPL.DYL.

Creating an object by category number

"pobj%=NEWOBJ(catnum%,clnum%)" creates a new object by category number "catnum%" belonging to the class "clnum%", returning the object handle on success or zero if out of memory. This keyword simply converts the category number supplied to a category handle using GETLIBH and then calls NEWOBJH.

Creating an object by category handle

"pobj%=NEWOBJH(cathand%,clnum%)" creates a new object by category handle "cathand%" belonging to the class "clnum%", returning the object handle on success or zero if out of memory.

Sending a message to an object

"ret%=SEND(pobj%,method%)" "ret%=SEND(pobj%,method%,var p1)" "ret%=SEND(pobj%,method%,var p1,var p2)" "ret%=SEND(pobj%,method%,var p1,var p2,var p3)" send a message to the object "pobj%" to call the method number "method%", passing between zero and three arguments depending on the requirements of the method, and returning the value returned by the selected method.

Protected message sending

"ret%=ENTERSEND(pobj%,method%)" "ret%=ENTERSEND(pobj%,method%,var p1)" "ret%=ENTERSEND(pobj%,method%,var p1,var p2)" "ret%=ENTERSEND(pobj%,method%,var p1,var p2,var p3)" send a message to an object with protection.

Methods which return errors by leaving must be called with protection.

ENTERSEND is the same as SEND except that, if the method leaves, the error code is returned to the caller; otherwise the value returned is as returned by the method.

Use ENTERSEND0 (described next) for methods which leave but do not return a value explicitly on success.

Protected message sending (returns zero on success)

"ret%=ENTERSEND0(pobj%,method%)" "ret%=ENTERSEND0(pobj%,method%,var p1)" "ret%=ENTERSEND0(pobj%,method%,var p1,var p2)" "ret%=ENTERSEND0(pobj%,method%,var p1,var p2,var p3)" send a message to an object with protection and guarantee that the known value zero is returned on success. Otherwise ENTERSEND0 is the same as ENTERSEND.

Methods which return errors by leaving but return nothing (or NULL) on success must use ENTERSEND0. Besides providing protection, ENTERSEND0 also returns zero if the method did not leave, or the negative error code if it did.

If ENTERSEND were incorrectly used instead and the method completed successfully (ie. without leaving), the return value would be random and could therefore be in the range of the error codes implying that the method failed.


Dynamic memory allocation

Overview of memory usage

For each running OPL program (or process) the operating system automatically allocates memory which can grow up to a maximum of 32 bytes less than 64K. The actual memory used, up to this limit, depends on the requirements of the process and is automatically grown or shrunk as necessary. This memory is called the process data segment and contains all the data used by the process as well as some fixed length data at low memory in the segment needed by the operating system to manage the process and for other system data.

Although the data segment for an OPL process contains several components, the only component of significant interest to the OPL programmer is the process heap. This section describes several keywords for accessing the heap.

The heap is essentially a block of memory at the highest addresses in a process data segment, so that the operating system can grow and shrink the heap simply by growing and shrinking the data segment and without having to move other blocks of memory at higher addresses in the data segment. The heaps of different processes are totally independent you need concern yourself only with the heap used in your own data segment.

The heap allocator

The heap allocator keywords are used to allocate, resize and free variable length memory cells from the process heap. Cells typically range in size from tens of bytes to a few kilobytes. Allocated cells are referenced directly by their address; they do not move to compact free space left by freed cells.

Heap allocator keywords are:

The heap structure

Initially, the heap consists of a single free cell. After a number of calls to allocate and free cells, the heap typically consists of ranges of adjacent allocated cells separated by single free cells (which are linked). If a cell being freed is next to another free cell the two cells are automatically joined to make a single cell to prevent the free cell linked list from growing unnecessarily.

Writing beyond the end of a cell will corrupt the heap's integrity. Such errors are difficult to debug because there is no immediate effect the corruption is a "time bomb". It will eventually be detected resulting in the process exiting prematurely by a subsequent allocator call such as "FREEALLOC".

Growing and shrinking the heap

The heap is not fixed in size. The operating system can grow the heap to satisfy allocation requests or shrink it to release memory back to the system.

Allocation of cells is based on "walking" the free space list to find the first free cell that is big enough to satisfy the request. If no free cell is big enough, the operating system will attempt to grow the data segment to add more free space at the end of the heap.

If there is no memory in the system to accomodate growth or if the data segment has reached its maximum size of (approximately) 64K, the allocate request fails. There are few circumstances when an allocate request can be assumed to succeed and calls to ALLOC, REALLOC and ADJUSTALLOC should have error recovery code to handle a failure to allocate.

Lost cells

There are cases in which programs allocate a sequence of cells which must either exist as a whole or not at all. If during the allocate sequence one of the later allocations fails, the previously allocated cells must be freed. If this is not done, the heap will contain unreferenced cells that consume memory to no purpose.

When designing multi-cell sequences of this kind, you should be mindful of the recovery code that must be written to free partially built multi-cell structures. The fewer the cells in such a structure, the simpler the recovery code is.

Internal fragmentation

The free space in the heap is normally fragmented to some extent; the largest cell that can be allocated is substantially smaller than the total free space. Excessive fragmentation, where the free space is distributed over a large number of cells and where, by implication, many of the free cells are small should be avoided because it results in inefficient use of memory and reduces the speed with which cells are allocated and freed.

Practical design hints for limiting internal fragmentation are:

The OPL runtime interpreter and the heap

The OPL runtime interpreter, which actually runs your program, uses the same data segment and heap as your program and makes extensive use of the heap. It is very important that you should understand the interpreter's use of the heap at least to a limited extent to avoid substantial internal fragmentation as described above.

Whenever an OPL procedure is called, a cell is allocated to store data required by the interpreter to manage the procedure. The same cell contains all the variables that you have declared in the procedure. When cacheing is not being used, the same cell also contains the translated code for the procedure which is interpreted. When the procedure returns (or implicitly returns due to an error) the cell is freed again back to the heap. This use of the heap is very tidy adjacent cells are allocated and freed with little opportunity for leaving gaps in the heap.

Unfortunately various other keywords also cause cells to be allocated and these can cause fragmentation. For example, LOADM, CREATE, OPEN etc. all allocate cells; UNLOADM, CLOSE etc. free those cells. If a procedure is called which uses CREATE to create a data file, the procedure cell is allocated, followed by the CREATE cell and the procedure cell is then freed when the procedure returns. The heap structure therefore contains a gap where the procedure cell was, which remains until all cells at higher addresses are freed.

Although a small number of gaps are not too serious and should eventually disappear in most cases anyway, the new heap allocating keywords provide ample opportunity to fragment the heap. Provided that you create and free cells in a careful and structured way, where any task needing the allocator frees them tidily on completion, there should not be a problem.

Warning peeking/poking the cell

Using the allocator is by no means simple in OPL since the data in an allocated cell usually has to be read or written in OPL using the PEEK and POKE set of keywords which are intrinsically subject to programming error. OPL does not provide good support for handling pointers (variables containing addresses), which are basic to heap usage, nor for complicated data structures, so that it is all too easy to make simple programming errors that have disastrous effects.

For these reasons, you are recommended to use the heap accessing keywords only when strictly necessary (which should not be very often) and to take extreme care when you do use them. On the other hand, for programmers with previous experience of dynamic memory allocation, the heap allocation keywords will often prove most useful.

Reasons for using the heap allocator

A few common instances where the allocator might be used are:

This concept will be familiar to you if you have used handles for the I/O keywords, where the handle references a cell used internally by the I/O system.

If you did not use the allocator in this case, you would probably need to declare a global array in the procedure calling the library procedures, with the disadvantages that the name and size of the array would need to be fixed for all time even when a better alternative mechanism has been devised for the library code with different data requirements.


Using the heap allocator

Allocating a cell

Use "pcell%=ALLOC(size%)" to allocate cell on heap of specified size returning the pointer to the cell or zero if there is not enough memory. The new cell is uninitialised you cannot assume that it is zeroed.

Freeing an allocated cell

Use "FREEALLOC pcell%" to free a previously allocated cell at "pcell%" as returned, for example, by ALLOC. Does nothing if "pcell%" is zero.

Changing a cell's size

Use "pcelln%=REALLOC(pcell%,size%)" to change the size of a previously allocated cell at address "pcell%" to "size%", returning the new cell address or zero if there is not enough memory. If out of memory, the old cell at "pcell%" is left as it was.

If successful, "pcelln%" will not be the same as "pcell%" on return only if the size increases and there is no free cell following the cell being grown which is large enough to accomodate the extra amount.

Inserting or deleting data in cell

Use "pcelln%=ADJUSTALLOC(pcell%,offset%,amount%)" to open or close a gap at "offset%" within the allocated cell "pcell%" returning the new cell address or zero if there is not enough memory. "offset%" is 0 for the first byte in the cell. Opens a gap if amount% is positive and closes it if negative. The data in the cell is automatically copied to the new position.

If successful, "pcelln%" will not be the same as "pcell%" on return only if "amount%" is positive and there is no free cell following the cell being adjusted which is large enough to accomodate the extra amount.

Finding out the cell length

Use "len%=LENALLOC(pcell%)" to get the length of the previously allocated cell at "pcell%".


Example using the allocator

This example illustrates the careful error checking which is essential when using the allocator. RAISE is used to jump to the error recovery code.

If you cannot understand this example it would be wise to avoid using the allocator altogether.

local pcell% rem pointer to cell
LOCAL pcelln%        rem new pointer to cell
LOCAL p%             rem general pointer
LOCAL n%             rem general integer
ONERR e1
pcell%=ALLOC(2+2*8)  rem holds an integer and
                     rem 2 8-byte floats initially
IF pcell%=0
  RAISE -10          rem out of memory; go to e1::
ENDIF
POKEW pcell%,2       rem store integer 2 at start of cell
                     rem ie. no. of floats
POKEF UADD(pcell%,2),2.72     rem store float 2.72
POKEF UADD(pcell%,10),3.14    rem store float 3.14
...
pcelln%=REALLOC(pcell%,2+3*8) rem space for 3rd float
IF pcelln%=0
  RAISE -10                   rem out of memory
ENDIF
pcell%=pcelln%                rem use new cell address
n%=PEEKW(pcell%)              rem no. of floats in cell
POKEF UADD(pcell%,2+n%*8),1.0 rem 1.0 after 3.14
POKEW pcell%,n%+1             rem one more float in cell
...
pcelln%=ADJUSTALLOC(pcell%,2,8) rem open gap before 2.72
IF pcelln%=0
  RAISE -10              rem out of memory
ENDIF
pcell%=pcelln%           rem use new cell address
POKEF UADD(pcell%,2),1.0 rem store 1.0 before 2.72
POKEW pcell%,4           rem 4 floats in cell now
...
p%=UADD(pcell%,LENALLOC(pcell%)) rem byte after cell end
p%=USUB(p%,8)              rem address of final float
POKEF p%,90000.1           rem overwrite with 90000.1
RAISE 0                    rem clear ERR value
e1::
FREEALLOC pcell%           rem free any cell created
IF err<>0
  ...                      rem display error message etc
ENDIF
RETURN ERR


<== Return to the Index