Pro LE Basic
This is where you create and test the functions that will be used in the User Function Modules. The functions executed by UF modules are actually very fast pre-compiled machine code evaluations of Functions that may include any combinations of other Functions, Sub Functions, Literals, and Variables.
Functions are the "programs" that are run by the UF modules. These have a Function Number, usually shortened to "Fn#," which corresponds to the drop down Fn# list on the UF modules. Whatever Fn# the module is set to, that is the function, or program, that is run when the module is calculated.
It can get a little more complicated than that, because some functions are able to run other functions, just as in any programming language. So Fn#s can actually be merely the starting point for a UF module, and it may actually run any number of other Fn#s.
Functions are edited in the large Text edit window. Double click on a Fn# (or select menu "Edit/Edit User Function") to place a Fn# into the Text edit window.
Sub Functions are not run directly by the modules, rather they are called by the functions as sub routines. They come in two flavors, built in and user defined. There is no practical difference between the two: Buillt in Sub Functions (often called simply "Functions") are pre defined; and user defined are created by you - they are esentially identically with the built in functions, but you get to define them your self.
Sub Functions have private variables, which means that the variables you define for a Sub Function are only used when the Sub Function is running, and will never conflict with Global Variables or variables in other Sub Functions, even when they have the same name.
Sub Functions are edited in the large Text edit window. Click on a Sub Function line to place it into the Text edit window.
Literals are simply numbers, or text in quotes. They have a value that is literally what you see.
Named Global Variables
Named Global Variables are numbers or text you define by giving them a name and an initial value; thereafter you use the name instead of the actual value (which you can change, hince the name "variable.").
There are two types of named global variables, numeric and text. You determine which type a variable is simply by giving it its initial value. If you give it a number, the variable is numeric; if you give it text (anything surrounded by single or double quotation marks) then it is a text variable. Once defined, a variable can not change its type. That is, if you define a variable as numeric by givinig it a numeric initial variable, you can not later give it a text variable, and vice versa.
Named Global Variables are created by placing their name in the top left Function Name box, and their initial value in the top right Variable value box. Click on any variable in the lower right Variables box to place it into the edit boxes.
There are several other variable types beside the Named Global Variables, which will be discussed later.
In the lower 2/3 of the Programmer are separate windows for each of the three programming constructs, Functions, Sub Functions, and Variables; while the top part has an editing Text window that you use to create or edit the Functions and Sub Functions..
Starting from the top, the first two input boxes, just under the control buttons, are the function name box (left), and variables box (right), with the edit window for the body of the function or sub function just below.
The large centeral window with the numbers, "0, 1, 2..." along the left are the stored Functions, and the box below that are the stored Sub Functions. Global variables are stored in the lower right box.
Any of these boxes may be resized by click & drag on the partitions between the boxes, and the entire Programmer window can be resized by click & drag on the lower right or on the edges in the standard Windows manner.
Clear - Erases all edit boxes: Text window, Function name box, Variable value box.
Test Fn - Test the Function in the Text window. If the function has an error, an error message will display at the top of the UF-Prog window, to the right of the menu selections. If no error, the result of the function will display in the UF-Prog caption, and the compiled function is temporarily placed in the currently highlighted Fn# slot, so that any UF modules set to that Fn# immediately will start running the tested function. If any text is highlighted, only the highlighted part will be tested, and it is tested for errors only, the function is not compiled and the highlighted Fn# slot remains unchanged.
Add Fn - copies the function in the Text window to the currently highlighted Fn# slot. If the slot is occupied a warning message is given (unless menu "View/Ask before overwrite" is unchecked). Add Fn runs an automatic test before sending it to the Fn# slot, and if there is an error it gives an error message and does not store the function.
Del Fn - erases the currently highlighted Fn# slot, and sets the slot's function to the default function (which is to add all module inputs and send the result to the module's output).
Open Fn - inserts a blank slot at the currently highlighted position, and moves all other slots down one. Slot #127 is lost, but if it contained text you are asked first (unless menu "View/Ask before overwrite" is unchecked).
Close Fn - deletes the currently highlighted slot and moves all slots up one.
Add Sub - copies the sub function in the Text window to the Sub Function window, placing it in alphebetical order by name. Click on a Sub Function line in the Sub Function window to place it into the Text edit window for editing.
Del Sub - removes the Sub Function who's name is in the Function name box, from the Sub Function list (in the lower left Sub Function window).
Test Sub - test the Sub Function in the Edit window for errors but does not compile or place it into the Sub Function list.
Add Var - Adds the Named Global Variables whos name is in the top left Function Name box, and initial value is in the top right Variable value box. Click on any variable in the lower right Variables box to place it into the edit boxes.
Del Var - removes the Named Global Variable who's name is in the Function name box, from the Variable list (in the lower right Variable window)..
New - Creates a new, empty UF workspace. All Functions, Sub Functions, and Named Variables are erased. Reset all Fn# slots to the default function, Out(In1+In2+In3+In4). New does not erase the current .ufn file, so you can recover from an accidental New by reloading the file.
Open - File Open dialog to load a previously saved UF Program file.
Save - Saves to the currently active .ufn file. Use Save As (below) to name or rename the .ufn file prior to using Save.
Save As...- File Save dialog to save the current UF Program data. These files are plain text and you can read them if you like but you should not modify them outside the UF Programmer. Doing so will very likely cause the Programmer to crash.
Note: If you save a UF Program file with the same name, and in the same directory as a SoftStep .ssp file, when you load the .ssp file the UF Program file will also load.
Save Default - save the current UF Programmer data into the default file. The default file is in the SoftStep program directory and is named "default.ufn." This file is loaded automatically into the UF Programmer when SoftStep starts up, but it is not automatically saved. If you want to update the default file, you must do it with this button.
Load Default - loads the default UF Programmer file into the current workspace. Previous workspace data is lost.
Import - pops up a window to import Functions, Sub Functions, and Named Variables from other .ufn files. Before importing, highlight the Fn# where the imported Functions will be inserted, then select the Import button. From the Import pop up, select the .ufn file to import from with the Open button, highlight the items you want to import, and click on the Import button. Only highlighted selections will be imported. Functions are inserted into the Function window starting with the Fn# that was highlighted, Sub Functions and Named Variables are inserted into their respectives windows in alphebetical order.
Reset - resets the math engine then recompiles the current UF Programmer data. This is not a file load function; it only recompiles the current data. Especially useful if you have been running a lot of tests and want to delete the test functions from whatever slots you used to test them. Also, with heavy editing, you may get an error message from the compiler saying it is out of space. Reset will defragment the space and allow you to continue.
Edit User Function- puts the highlighted Fn# slot into the top line Text window. Any text currently in the Text window is lost. Same as double clicking on a Fn#.
Update/Add Function - same as the Add Fn button.
Delete/Erase Fn Slot - same as the Del Fn button.
Open Function Slot - same as the Open Fn button.
Close Function Slot - same as the Close Fn button.
Add Sub Function - same as the Add Sub button.
Delete Sub Function - same as the Del Sub button.
Add Named Variable - same as the Add Var button.
Delete Named Variable - same as the Del Var button.
Always On Top - when selected (checked), the UF Programmer window stays on top. If you get a conflict with another pop up window, it usually can be resolved by clicking the icons in the Windows Task Bar (where Windows Start is).
Ask Before Overwrite - if not selected (no check) you are not asked before overwriting a slot that contains text.
Insert Prototype - when selected (check) a function prototype selecred in the Functions menu is inserted into the Text window at the cursor. If not selected the function prototype is Copied into the Windows Clipboard, for a subsiquent right-click-Paste into the Text window.
Remember Layout - You can change the size of the UF-Prog window as well as the individual window panes within it, by click-and-drag on the edges. When selected (check), the layout is remembered next time you run SoftStep.
Stop UF Modules- when selected (check), UF modules do not process.
Select Edit Box Font - calls the Windows Font Selection Dialog, where you can select the font type and size for the Text window. Only fixed width fonts are listed for selection.
Show Text Arrays- pops up a read only text window from which you can view the text saved in the 10 text sets used with the Text Functions. The popup window has buttons numbered 0-9 representing the text sets. If a set has text in it the button will be active, if not available the button is grayed out and inactive. Click on the numbered button to see the text; click on the button labeled "Show Hash" to view the unique hash number associated with each line.
All functions available in the UF-Programmer are listed in the Functions Menu, along with dummy parameters for the function and a brief description of it. The function name with dummy parameters is called a function prototype. Selecting one of the functions will insert its function prototype at the cursor location or (if Insert Prototype not selected) copy it to the Clipboard. Selecting a function does not run it, it only puts the text into the Text window. None of the Functions menu selections actually run the functions, they are only used to select the function prototypes.
There are three separate steps to making an active User Function module:
It is not necessary to do these steps in order. In fact they generally are done in an interactive process, more or less simultaniously. But it is helpful to be aware of them as separate processes at the outset.
To start with a simple example, we will create a module that does no more than send a value to its output:
To continue, add to the above to create a function that sends the minimum of two inputs to the output:
This is a useful function that you will use again, so save it in one of the Fn# slots:
Next, we will create a 2nd output that gives the maximum of the two inputs: But before we can get input values from the first module into another module, we need to save them in temporary variables. So before going on, we will experiment a little with variables.
You can define a variable with any name you want, as long as it starts with a letter character and contains only the letters A-Z, the numbers 0-9, a dot or an underscore. Case does not matter, and will be converted to upper case when evaluated.
You can create a variable either by declaring it with the Add Var button or by using it as a Sub Function variable. Sub Function variables are local to the sub function; Variables created with the Add Var button are global and may be accessed by any function. In addition to the variables you define, you can also use the predefined letter name variables, A to Z. There also are other variable types and variable-like functions that will be described later.
In addition to creating variables, you also have to be able to change their values under program control - otherwise they are constants, not variables. The Let() function does this, as well as the Letter-dot function which works as a shorthand Let() for letter name variables. Both of these will be discussed in the tutorial section below.
Experiment with creating and testing some variables:
Now that you are familiar with Variables, we can continue to create a separate Max output for the previous Min function.
For the last part of this tutorial, we will create and use a sub function. For the example we will create Steve Whealton's binary reflected gray code transformation, which is a useful algorithmic number series. Click here to read what Steve has to say about it,
The BRGC algorithm is quite simple: do an integer division of the input, and XOR the result with the original input. It is a good example of how you can quickly create a module to do a specialized function.
Precedence and Grouping Items
All functions and sub functions return a value when they are evaluated (run). Some functions are not run for the value they produce, but tor the "side effect," and usually these functions will simply return 0 after doing whatever they do. Many of these will return a different value if an error is encountered. The particular error behaviour of a function will be listed in the function documentation.
When you enter multiple items separated by a space, the compiler assumes multiplication, and will multiply the results. To avoid this, you should always separate multiple expressions with commas.
You can have comma separated multiple items in both Functions and Sub Functions. Within a single function, you may have up to 32 comma separated items.
You can use multiple lines for your Function and Sub Function definitions. You can insert comments on any line, including empty lines. A comment starts with a semicolon ";" and continues until the end of the current line. Anything between the first semicolon and the end of the line is ignored by the compiler.
Items separated by a comma are evaluated left to right, and the expression result is the last item evaluated. Within a comma field, items will be given the following operator precedence from highest to lowest:
Anything inside parenthesis is performed first () Factorial, percentage !, % Exponentiation ^ Negation (unary) - Multiplication, division *, / Integer division \ Other operators Modulo (remainder) MOD Addition, subtraction +, - Relational operators <, >, >=, <=, =, <> AND operator OR, XOR (exclusive or) EQV (equivalence) IMP (implication)
When consecutive operators have the same priority, they are evaluated from left to right. This means that an expression such as "a-b-c" is evaluated as "(a-b)-c". Generally, it is a good idea to make liberal use of parentheses to be sure your expression is being evaluated the way you want.
The compiler supports notations for binary, octal, and hexadecimal numbers. These numbers must be preceded by the character # followed by b, o, or h for binary, octal, or hexadecimal. The prefixes are part of the number following, so no space is allowed. With #h legal numerals are 0-9 and A-F; with #o, only 0-7 are allowed; and with #b only 0 and 1 are allowed.
The default number type for the UF Prog compiler is double precision floating point. Unless otherwise noted, you may assume functions, variables, etc. will be in that format and they will automatically be converted to and from the byte integer number type used by SoftStep modules. Some functions and variables, notably those dealing with LED bits and other binary bitwise manipulations use 32 bit integers.
Double precision floating point numbers have an enormous range: -1.79769313486232E308 to -4.94065645841247E-324 for negative values; 4.94065645841247E-324 to 1.79769313486232E308 for positive values. 32 bit integers have a much smaller range of -2,147,483,648 to 2,147,483,647.
Values for True and False
Unlike SoftStep modules, which return 127 for True, UF-Prog compiler relational operators (>, <, <=, >=, =, <>) return a 1 for True. False is 0 in both systems.
Sub Function Parameters
Sub Functions may have up to 32 parameters, which are listed in the Variables box (to the right of the Function Name box), separated by commas, when the Sub Function is created. To make a parameter optional and/or to simply use it as a temporary variable, create it with a predefined value such as: "foo=0."
UF modules have no output until you give them one with the Out() function. Just running an evaluation can do lots of things, but it does not send anything to the module's output. Only Out() does that.
Here is how the UF modules work. First, they are linked into the priority chain like any module, and they are run in the same order you find them in the menu, like all the other modules. When the UF module is run, it first sends to the math engine its unique module id, plus the values at its inputs and the value of its clock-on value. These become available in the In1-In4 and InC variables. Then it runs the function pointed to by its Fn# setting. When the function returns, several bit flags may have been set to tell the module what kind of updating it needs to do, without the module having to waste time asking. One of those will be the Out flag, if the Out function was somewhere in the function. If that flag is set, the module sends the value to its output, which makes it available to any other modules that are connected to it, and it updates its output box. Then it clears the bit flags so it won't waste CPU cycles updating the same thing over and over.
Out() can be used anywhere in the function, but as the function is evaluated left to right, anything to the right is not seen. If you use Out() more than once in a function, only the last one to be evaluated is seen by the module. Like any function, you can put any legal function inside the parentheses: a number, a variable, or a Sub Function. For example to send a random number between 0 and 127, use: OUT(RAN*127)
Prev (no parenthesis) is nothing more than the module's previous output, before the Out() function writes to it for the current cycle. Thus, for Prev to be useful you must use it before invoking Out().
As described above, these variables are set by the module prior to calling the function. They can not be changed directly. In1 to In4 (to In16 for UCXV and UFXV modules) are the modules's inputs, and InC is the value of the clock input when it is something other than 0. The following example reads module input 1, halves it, and sends it to the output: Out(In1 /2).
GetSlider(mID, Slider) - Return value from UF Slider. mID is the UF Slider module ID -1 (module 1 has mID of 0, 2 of 1, etc.); Slider is the slider number in the module, starting with 0. There are up to 63 sliders but fewer may be displayed.
PutSlider(mID, Slider, Value) - Writes the Value given into the Slider and mID given. Values are floating point, not limited to the 0-127 MIDI range, and may be negative. When a value is written to the slider, the slider graphics updates. This allows the UF Sliders to be used as a graphic bar display as well as an input interface.
SliderRange(mID, Slider, Range, Offset) - Sets the Range (any value => 0.1) and Offset (any value) for the Slider and mID given. Offset is optional, and defaults to 0.
Run is the state of the Run and Stop system flags, and the corresponding state of the Run/Start button on the main SoftStep screen's tool bar. When the button is pressed (Run) the value is True (1), when unpressed (Stop), it is False (0).
Note that while Run can only read the status of the Run/Stop flag, there is a function that will set the system Run/Stop status, just as if the tool bar button had been clicked. This is the RunStop() function, described in the Clocks section..
Start is a system flag that goes to True (1) briefly when the clocks are restarted, then returns to 0. System clocks are restarted when Tempo is changed, when a new file is loaded or saved, when the All Notes Off button is released, and at other times where the system clocks are realigned - it usually can be taken to signal the beginning of a composition.
CPS() gives direct access to the Control Panel Sliders, in the Floating Control Panel, which is available from the SoftStep tool bar. There are 16 sliders numbered 0-15,
UFOMouse(mID, Which) - reads mouse position and flag when mouse button is clicked in the label part of the UFO Module. This function can be used only with the UFO module, and before it can be used the module must be set to show its label instead of the LED. Do this with nLED(4). When the mouse is clicked on the label part of the module, you can read the mouse position while it is being moved across the screen. Mouse position will be readable as long as the mouse button is depressed, even after the cursor leaves the label area. This function is useful for creating a mouse interface that is not constrained to the 0-127 range of standard SoftStep modules.
mID identifies the UFO module to read from. This value is the same as the module number less 1. That is, UFO-1 would have an mID of 0, UFO-2 would be mID 1, etc.
Which sets the mouse parameter to return:
0=mouse clicked flag (1 if any mouse button is pressed, 0 if no mouse button is pressed),
1=mouse button value (normally this will be 1 for left button, 2 for right button and 3 for both buttons),
2=shift value, when a keybard shift key is pressed while the mouse button is down (1=Shift key, 2=Ctrl key, 4=Alt key),
3=the mouse X value relative to the point in the label where the mouse button was first pressed, with 0 at the click point, negative numbers to the left of the point, and positive numbers at the right of the point,
4= the mouse Y position with negative numbers above the click point, positive numbers below.
Two functions are available to assign values to variables: LET() and N.() (where N is any letter A to Z). The syntax is: LET( Name , Value).
The letters A to Z are pre defined as variable names, and may be used exactly as any other variable. However they also can be used with the N.() ("Let-Set") function. Here, you just give the letter name and a dot and the enclosing parentheses, which hold the value to be assigned. For example A.(A+1) adds 1 to A every time it is called. This function is slightly faster than LET().
UF Modules have 10 private variables available to them, that can not be accessed by any other module. These are predefined varialbes named V0 to V9. Assign values to the variables with: V0.() as with N.() above.
Up to 127 modules may use private variables. In the unlikely event that more than 127 modules are using private variables, then the variables from the 127th module on are shared. See obscure note in the next section for details.
There is a 16 cell memory array privately available to each user function module created. It is private memory that other modules cannot access or change. If you need to pass values to other modules, use standard global variables, but if you need, for example, to track inputs over time, you would use the Put/Get array.
Obscure note: in order to conserve system memory, Put/Get memory is assigned to modules only when they use it, up to a total of 127 memory slots. This does not limit the total number of user function modules you can create, but it does limit the total number of private Put/Get slots to 127. While it is highly unlikely you would exceed 127 modules using Put/Get, any modules beyond that number will share Put/Get slot 127. The assignment of Put/Get slots to modules is entirely automatic, and you do not need to track which physical slot is being used by a particular module.
Put(Step , Value) stores the Value into the given Step 0-15.
Get( Step) fetches the Value at the given Step 0-15.
Ins(Value) moves Steps 0-14 down 1; Step 15 is lost; Value is stored in Step 0.
There is a global memory array available for temporary storage, accessible to all modules at all times. This is configured as 8 pages (numbered 0-7) of 1024 steps (0-1023). The storage type is double precision floating, so any value can be put into these tables.
Store(Page, Step, Value) stores the Value into the given Page (0-7) and Step (0-1023).
Fetch(Page, Step) fetches the Value stored at the given Page (0-7) and Step (0-1023).
This table is similar to the Global Memory Array above, but it is configured as 128 pages (numbered 0-127) of 128 steps (0-127), to make it convient to use to translate Module input values of 0-127 into arbitary sized UF values.
In addition to the StoreX() and FetchX() functions which work the same as the above Store() and Fetch() functions, there is a MakeXTable() function that creates an exponential value table from 1 to the range you specify. This spaces values at the start of the table close together, and values toward the end of the table farther apart.
MakeXTable(Page, Range) - Make 128-step exponential translation table from 1 to Range. Step 0 is always 1, and the table exponentialy increments from there up to and including Range.
StoreX(Page, Step, Value) stores the Value into the given Page (0-127) and Step (0-127).
FetchX(Page, Step) fetches the Value stored at the given Page (0-127) and Step (0-127).
The File/Link functions act both as regular file read/write functions that give great flexibility to the data format, and also they can be used to make direct communication links with multiple instances of SoftStep, or with SoftStep and other programs that support link files, such as the Analog Box2 soft synth. Links may be made with multiple applications on the same computer, or with different computers connected in a Local Area Network.
File/Links look like random read/write files, and in fact you can open any file as a link file, or you can ignore the link capability and just use the File/Link functions to access data files of your own choosing. To open a network connection, simply open the link file as a networked file.
To make a file link between two (or more) applications, open the same file with each application you want to link, then treat the data locations (records) as input and output channels. When sending data, simply write to the location, which effectively makes it an output channel relative to the application doing the writing. When reading data generated by another application, read from the location the external application is writing to. For example, one program might treat file location 1 as the outpt channel, writing data to that location; and it would treat file location 2 as the input channel, reading data from that location. Then a second program would treat the two locations the reverse: location 1 would be the input channel, and location 2 would be the output channel.
In order for one program to accurately read data generated by another program the reading program must somehow insure it is getting the data accurately, without skipping. For SoftStep you can do this easily by using the Absolute A or B clocks, and setting the clock rate at least 2 times the MIDI clock rate. You can use MIDITick to get the MIDI clock rate for the current Tempo.
GetFileName(StringVariable) - pops up a file dialog box, the selected file name is inserted into the given string variable. String variables must be predefined using quotes. The file is not opened or changed in any way. You may select filenames from other computers in the network. Returns 1 if the dialog is canceled without selecting a file, otherwise returns 0.
FileExists("filename") - returns 1 if the given filename exists, 0 if it is not found. File names may be literal (in quotes) or a string variable, and may be name-only ("name.ext"), full name+path ("c:\directory\name.ext"), or a network name ("\\computer\alias\directory\name.ext"). If name-only is given, the current directory (the last directory used to load or save a .ssp file) is used.
OpenFile("type", "filename", handle) - Opens a file for linked read or write. Returns 0 if file is successfully opened, 1 on error. If the file does not exist, one will be created. Any opened files are closed automatically when SoftStep exits, when a new .ssp or .ufn file is loaded, or when a handle is reused. All parameters are optional.
"Type" is a string literal or string variable that defines the data type of the file. Valid type names are:
- Byte - 1 byte per record, 8-bit unsigned integers 0-255, or text characters. Byte is the default type.
- Word - 2 bytes per record, 16-bit signed integers -32,768 to 32,767.
- Long - 4 bytes per record, 32-bit signed long integers -2,147,483,648 to 2,147,483,647.
- Single - 4 bytes per record, 32-bit signed single floating point- 3.402823E38 to -1.401298E-45 for negative values; 1.401298E-45 to 3.402823E38 for positive values.
- Double - 8 bytes per record, 64 bit signed double floating point -1.79769313486232E308 to -4.94065645841247E-324 for negative values; 4.94065645841247E-324 to 1.79769313486232E308 for positive values.
"Filename" is the name of the link file to be opened. File names may be literal (in quotes) or a string variable, and may be name-only ("name.ext"), full name+path ("c:\directory\name.ext"), or a network name ("\\computer\alias\directory\name.ext"). If name-only is given, the current directory (the last directory used to load or save a .ssp file) is used. Filename defaults to "LinkFile.dat" in the current directory.
Handle is a value from 1 to 128 that defines the opened link file to be used with ReadFromFile() and WriteToFile() below. Defaults to 1. Use handle if you are going to use more than 1 link file at a time, up to 128 simultaneously opened files. It does not matter what number you assign the handle, as long as you use the same number for subsequent reads and writes. If you open a file with a handle that is currently in use, the open file is first closed before the handle is reused.
WriteToFile(value, position, handle) - Write a record to a link file previously opened with OpenFile(). Returns the value written. All parameters are optional
Value must be in the range of the data Type defined when the file is opened, and defaults to 0.
Position is the record number and may be any positive value starting from 1. Care should be taken when using large position values as all unused records up to the position given will be saved when the file is closed. Defaults to 1.
Handle is the value from 1 to 128 that was used to open the file. Defaults to 1.
ReadFromFile(position, handle) - Read a record from a link file previously opened with OpenFile(). Returns the value of the record at the position given, of the file opened with the handle given. Both position and handle are optional and default to 1. Values returned are within the range given by the Type set when the file is opened. If a position is is outside of the range of previously written records, the return value is undefined.
Arithmetic operators: Add, Subtract, Multiply, Divide, Integer Divide, Modulus (remainder). Except for MOD, these are not space sensitive. For example, you can use either A+B or A + B. The backward slash "\" is integer divide, which divides without rounding.
Parentheses prioritizes an expression. Use them liberally to clarify to both you and the computer how you want your expressions evaluated.
Random Numbers. Except for LRan, these all produce a floating point random number between 0 and 1. Multiply them by the maximum range you want. For example RAN * 127 gives a random number between 0 and 127.
Ran is an evenly distributed random number; nRan is a repeatable random number, and rW is 1/f random (Random Walk).
Both nRan and rW use 16K random number tables as their source. These tables are generated freshly every time SoftStep starts up, and the values of the tables depends on the Options menu Ran Seed value. These are the same tables used by the Pattern Rand module.
Within the table, you can set the starting point for the nRan and rW lookup with the functions RRSeed(N) and RWSeed(N). These both take a single number from 0-16383, they set the seed index that will be used by subsequent calls to nRan or rW, respectively, and return the random number at that index. Thus, you can use the seed functions to either set a new seed for nRan and/or rW - and you can also use them as indexed random number generators themselves to get the numbers out of the table in all kinds of sequences - simple forward and backward, as well as stepping by arbitrary amounts.
LRan returns a 32 bit integer with random distribution of the bits set to about 25%; it is most useful for setting up a random LED display.
These are bitwise operators that return results on a bit by bit basis. Numeric values are automatically converted to 32 bit integers for the operations, then converted back to double precision floating point to return the results.
Here are the logical operators with examples:
|NOT||Bitwise NOT||NOT(15) = -16|
|AND||Bitwise AND||#b101 AND #h1E=4|
|OR||Bitwise OR||13 OR 6 = 15|
|XOR||Bitwise Exclusive OR||9 XOR 3 = 10|
|EQV||Bitwise Equivalence||6 EQV 9 = -16|
|IMP||Bitwise Implication||1 IMP 5 = -1|
Here is the bitwise truth table:
|NOT(1) > 0||1 AND 1 > 1||1 OR 1 > 1||1 XOR 1 > 0||1 EQV 1 > 1||1 IMP 1 > 1|
|1 AND 0 > 0||1 OR 0 > 1||1 XOR 0 > 1||1 EQV 0 > 0||1 IMP 0 > 1|
|NOT(0) > 1||0 AND 1 > 0||0 OR 1 > 1||0 XOR 1 > 1||0 EQV 1 > 0||0 IMP 1 > 0|
|0 AND 0 > 0||0 OR 0 > 0||0 XOR 0 > 0||0 EQV 0 > 1||0 IMP 0 > 0|
Unlike the bitwise logical operators above, these relational operators work on the parameters as a whole and (except for IIF(), discussed below) they return True (1) or False (0) The operators are: < (less than), > (greater than), = (equal test), >= (greater or equal to), <= (less or equal to), and <> (not equal to).
The Equal Test operator may be either a single equal sign ("=") or a double one("==").
IIF() is a special function that is defined as: IIF(condition, ifTrue, ifFalse). If the value of condition is True, then the value of the ifTrue parameter is returned, else the value of the ifFalse parameter is returned. Both ifTrue and ifFalse parameters are evaluated, so this function can not be used as a switch. However there is a Switch function, discussed below.
Here are the relational operators with examples:
|>||Greater than||9 > 2 = 1 * see note|
|<||Less than||7 < 4 = 0|
|==||Equal test||5 == 4 = 0|
|>=||Greater or equal||3 >= 3 = 1|
|<=||Less or equal||#h3E <= 9 = 0|
|<>||Not equal||#b10101 <> 20 = 1|
|IIF||If condition||IIf(1+1=2,4,5) = 4|
MIN(N1, N2) returns the smallest value; MAX(N1, N2) returns the largest. You are not limited to just two parameters - you can use any number up to 32. Separate each parameter to be tested with a comma.
Wrap(N) performs a Mod 128 so if the parameter goes above 127, it is wrapped around. Lim(N) limits the parameter to no less than 0 and no greater than 127. Any values sent to Out() are automatically processed with LIM() so it is unnecessary to use the function as the final value to Out().
Edge(x, y, w, h, x2, y2, [x1], [y1]) - Test if a rectangle defined by x=Left, y=Top, w=Width, h=Height hits an edge of a larger rectangle defined by x1,y1, x2,y2. Both x1 and y1 are optional and default to 0. Returns 0 if the smaller rectangle is wholly within the larger. If overlap, returns the additive combination of: 1 for an East wall hit, 2 for West, 4 for South, and 8 for North.
FitScale((Note, ScaleType) - returns the given note value quantized to the given scale. Scales are the same as the Quantize module: 0=none, 1=Major, 2=Minor, 3=Pentatonic, 4=Whole Tone, 5=Fifths+Octaves, 6=Octaves, 7=Major Triad, 8=Minor Triad. Anything outside the 1-7 selection range leaves the input unchanged.
A full set of double precision trig functions are available, including
Arc (Inverse) and Hyperbolic functions. Pi is provided as a double precision
constant with the value of: 3.14159265358979 (actually, it is a variable, and
you can redefine it if you like with Let()) Also defined as constants are:
e (spelled here as _E) = 2.718281828459,and
Phi = 1.61803398875.
The default mode for trig functions is Radian; but you can change that by entering Radian or Degree or Grad (Gradient) for the mode you want. These functions return a number indicating the new mode: 1 for Radian, 2 for Degree, and 3 for Grad. If you want to query the current mode, use the special variable TrigMode (no parenthesis), which returns the current mode as above but does not change it.
Here is the complete list of trig functions, with examples:
|SIN||Sine||sin(pi) = 0 *see note|
|COS||Cosine||cos(pi) = -1|
|TAN||Tangent||tan(pi) = 0|
|ASIN||Arc sine||asin(1) = 1.570|
|ACOS||Arc cosine||acos(-1) = 3.141|
|ATAN||Arc tangent||atan(0) = 0|
|SEC||Secant||sec(0) = 1|
|CSC||Cosecant||csc(1) = 1.18|
|COT||Cotangent||cot(1) = 0.642|
|SINH||Hyperbolic sine||sinh(3) = 10.01|
|COSH||Hyperbolic cosine||cosh(2) = 3.76|
|TANH||Hyperbolic tangent||tanh(1) = 0.76|
|COTH||Hyperbolic cotangent||coth(1) = 1.31|
|SECH||Hyperbolic secant||sech(0) = 1|
|CSCH||Hyperbolic cosecant||csch(1) = 0.85|
|ASINH||Hyperbolic arc sine||asinh(2) = 1.44|
|ACOSH||Hyperbolic arc cosine||acosh(9) = 2.89|
|ATANH||Hyperbolic arc tangent||atanh(.1) = 0.10|
|ACOTH||Hyperbolic arc cotangent||acoth(7) = 0.14|
|ASECH||Hyperbolic arc secant||asech(.3) = 1.87|
|ACSCH||Hyperbolic arc cosecant||acsch(2) = 0.48|
|Radian||Set mode to Radian||Radian = 1|
|Degree||Set mode to Degree||Degree = 2|
|Grad||Set mode to Gradient||Grad = 3|
|TrigMode||Return current mode||TrigMode = (1 to 3)|
A full set of double precision floating point math functions are available:
|EXP||e to the power of||exp(3) = 20.08|
|EXP2||2 to the power of||exp2(3) = 8|
|EXP10||10 to the power of||exp10(3) = 1000|
|LOG||Natural log||log(16) = 2.77|
|LOG2||Log base 2||log2(8) = 3|
|LOG10||Log base 10||log10(100) = 2|
|^||Raised to the power of||4 ^ 5 = 1024|
|!||Factorial||5! = 120fact(5) = 120|
|SQR||Square root||sqr(64) = 8|
|%||Percentage||35% = 0.35|
|ABS||Absolute value||abs(-8) = 8|
|CEIL||Round up||ceil(6.2) = 7|
|INT||Truncate to an integer||int(6.8) = 6|
|Fract||Return fractional part||fract(6.8) = 0.8|
|SGN||Sign of expression (-1, 0, or 1)||sgn(-9) = -1|
Some of the UF modules have LEDs (graphical Light Emitting Diodes) that you can directly control. Select how many LEDs to show with nLED(), and turn the LEDs on or off with LED(). Note that all LED related functions and variables are 32-bit integers, not the floating point values normally used by the UF-Prog compiler. Conversion to and from the two formats is automatic, but you should be aware of the much smaller range of the 32-bit integers to avoid overflow errors.
The ULR, and UFX modules each have a single row (ULR) or column
(UFX) of LEDs, so controlling them is very straightforward: you simply call
LED() with a binary number: the 1s turn the LEDs on, and the 0s turn
them off. So, for example to set one of these modules to show 8 LEDs, and to
turn every other one on, you would use:
nLED(8) , LED(#b01010101).
When you save a ULR or UFX that has LEDs set up, the configuration is saved and when loaded again it will have the same size and number of LEDs that it was saved with, but the LEDs themselves are considered indicator lights, and their ON/Off values are not saved - when restored the LEDs will always be Off.
The ULM module has a matrix of up to
32x32 LEDs, so there is a bit more involved in controlling them. To select how
many LEDs to show, use nLED() twice. Once for the number of columns, and
again with a negative number for the number of rows. It does not matter whether
you give rows first or columns first, just that you use a positive number for
columns and a negative number for rows. So to set a LED matrix of 8 rows by 12
columns, you would use:
The UCXV & UFXV modules each have 16 LEDs numbered 0-15 which you can control with the LED() function. The number of LEDs showing is the same as the number of LEDs showing, so nLED() sets both at the same time. The UCXV also has an extra LED opposite the Clock input. This is effectively LED number 16, but to make it easier to control this as a clock indicator, separate from the other LEDs, there is a special function for this module:
ClkLED(Clk, LEDs) - controls LEDS in the UCXV module. Set Clk to 0 to turn the clock led off, and any non zero to turn it on; set LEDs to control the other module LEDS, same as with the LED() funcction.
The LED matrix of the ULM is an active component of the module, not just a passive display. You can test them with the mouse, click them on and off, and even draw on them. See the ULM section for details. Because of this, the ON/Off status of the ULM is stored when saved, and restored when loaded again.
Matrix LED functions use the concept of a LED Matrix Array of 32, 32-bit integers . The bits represent the on/off status of a row of LEDs, and the index into the array (0-31) select the row.
Matrix LEDs turn on or off by reading the entire array at once. To write to a particular LED row, you use bits set to 1/0 for on/off just as in the other modules, but you also specify which row (0-31) - and you do not write directly to the LEDs with the LED() function, instead you write into the module's LED Matrix array; then when you are ready to display the LEDs, you use LED() as a command to update the LED display according to the values in the LED Matrix array. Whenever LED( N ) is called, and N has changed, the module will update its LEDs. It does not matter what the actual value of N is, only that it has changed from the previous value. No update is performed if N has not changed, to conserve CPU cycles.
Although only ULM modules have matrix LEDs, any of the UF modules can have its own LED Matrix array, and can use the LM functions described below. This makes the LED matrix memory array similar in concept to the Get/Put memory array, with the same automatic assignment rules (see obscure note below). There are two important differences between Get/Put memory and LED Matrix memory: The first is that Get/Put memory is double precision floating point, which is the natural number type of the compiler. This means there is no restriction as to what you can store in Get/Put. LED Matrix memory is 32-bit integer memory, which should only be used for bit type or pure integer storage. So, while you can store SoftStep MIDI values in LED Matrix memory (they are integers), you cannot store floating point values. The second difference is that the Get/Put array is 16 steps deep, while LED Matrix memory is 32 steps.
Obscure note: in order to conserve system memory, LED Matrix memory is assigned to modules only when they use it, up to a total of 127 memory slots. This does not limit the total number of user function modules you can create, but it does limit the total number of private LED Matrix slots to 127, any modules beyond that number will share slot 127. The assignment of slots to modules is entirely automatic, and in most cases you do not need to track which physical slot is being used by a particular module. If you do need to know which slot a particular module is using, use the special variable UID.
The LED Matrix functions are:
LM.( row, bits) - to write the bits in one of the possible 0-31 rows.
LM( row ) - to read the bits in a particular row. Notice there is no dot. LMdot() for writing and just LM() for reading, like the letter variables.
LMI(bits) - to insert bits into the top, and move all the other rows down. The last row is lost.
LMRan (no parentheses) - to fill the array with random bits of about 25% density. Returns the value of the 31st row.
LMClr (no parentheses) - to fill the array with 0s.
LMCRC (no parentheses) - returns a unique number (the CRC) of the array, to allow for quick tests of whether any bits have changed. This is useful when running the Life algorithm, to detect when it has become static. The CRC method is fast and dirty: simply the cumulative XOR of each of the 32 rows. This never fails to alert (by reporting the same number) if the array has not changed, but it occasionally will report two different arrays as being the same.
LMBit(page, row, column) reads the bit status (0/1) at the Page (0-127), Row (0-31) and Bit Column (0-31). "Page" in this context is the assigned slot number 0-127. All modules that use the LED Matrix memory are assigned a unique ID number the first time they access it, which remains the slot number used by that module for the duration of the session (although it can and usually will change from session to session). You can access the module's slot number with the special variable UID.
LMRow(Page, Row) - read the bits in a particular page and row. Page is 0-127, row is 0-31. Same function as LM() above, but can read from any page, not just the one "owned" by the module.
LMWrite(Page, Row, bits) - write the bits in a particular page and row. Page is 0-127, row is 0-31. Same function as LM.() above, but can write to any page, not just the one "owned" by the module.
LMCopy( source , destination ) - source and destination are the slot (Page) numbers 0-127. While the slots are assigned to modules on an as-needed basis, all 128 array slots are allocated, so you can safely use the higher slot numbers however you like, such as storing a LED Life seed pattern for instant non random restarts of the Life algorithm. Slot 0 is never assigned to any module, so it is always available as shared memory.
Here is how you would set a ULM module to
a matrix of 24 x 16 LEDs, and light them up randomly. You can copy and paste
this directly to the UF-Prog Text window and click on the Test Fnbutton
to run it. Be sure you have the ULM module Fn# set to the same value as
the UF-ProgFn# slot .
NLED( -16 ) , NLED( 24 ) , LMRAN , LED( 1 )
Or, if you want to get wild: NLED( -16 ) , NLED( 24 ) , LED( LMRAN )
These functions allow pixel access to SoftStep Image modules (or the Info Picture module for the InfoPix function only). They are not particularly fast and should not be used for any significant drawing operations; rather they are intended to allow you to access pixels in MIDI time to use them in your music algorithms. The functions require a module ID ("mID") number so they know which module to access. This value is the same as the Image module less 1. That is, Image-1 would have an ID of 0, Image-2 would be ID 1, etc. Read Info Picture pixels
InfoPix(mID, X, Y) - reads the pixel value of an InfoPicture moduleimage at X, Y.Pixels are 24 bit integers with 8 bits each for Red, Green, and Blue.
All the following functions are for the Image module:
RPix(mID, X, Y) reads the pixel value at X, Y. Pixels are 24 bit integers with 8 bits each for Red, Green, and Blue.
WPix(mID, X, Y, Color) writes the color at X, Y.
RGB ( Red, Green, Blue) returns the 24 bit color integer from the
RGB components. RGB is each 0-255. For example the following will write a
bright green pixel in Image-1 at 40, 10:
WPix(0, 40, 10, RGB(0 , 255, 0))
ClsPix(mID) erases the image module picture, setting the background to 0 (black). Not included in the calculator keypad buttons.
Pen(mID, N ) sets the pen to any of the 16 available drawing modes. The default is 13-Copy Pen, which just draws the color. Here is a list of all 16:
|2||Not Merge pen|
|3||Mask Not pen|
|4||Not Copy pen|
|5||Mask pen Not|
|8||Not Mask pen|
|10||Not Xor pen|
|12||Merge Not pen|
|14||Merge pen Not|
The compiler supports a full set of bit manipulations: shifts, rotates, and bit set and read operations on 7-bit MIDI bytes, 8-bit bytes, 16 bit words, and 32 bit long words.
Operations specific to the different integer sizes have"M, B, W, or L" appended for an operation on a MIDI byte, a byte, a word or a long. So for SHL (shift left) there is SHLM, SHLB, SHLW and SHLL for each of the integer sizes. This description of the bit functions will simply refer to the operation without the appended size descriptor. It is assumed that the argument "N" is an integer of the appropriate size, which will be returned modified by the operation.
All the single shifts and rotates save the bit that was shifted out in an internal Carry variable, much as a CPU chip does. So you can use a shift, which will lose the bit, then a rotate with carry which will bring it back. By convention, bits are numbered from 0, right to left, with the 0 as least significant bit. Thus right shifts are like divisions by 2; left shifts are like multiplies by 2.
Single bit shift operations with Carry:
SHL(N) - Shift Left: shifts one bit to the left. MS bit is lost
(but held in Carry), Bit 0 gets 0.
SHR(N) - Shift Right: shifts one bit to the right. MS bit gets 0, Bit 0 is lost (but held in Carry)
RL(N) -- Rotate Left: shifts one bit left, with MS bit moving into Bit 0, and also saved in Carry.
RR(N) - Rotate Right: shifts one bit right, with Bit 0 moving into the MS bit position, and also saved in Carry.
RCL(N) Rotate through Carry Left: shifts one bit left, with the Carry bit moving into Bit 0, and the MS bit lost (but held in Carry).
RCR(N) - Rotate through Carry Right: shifts one bit right, with the Carry bit moving into the MS bit position, and Bit 0 lost (but held in Carry).
Multiple bit shift operations without Carry:
SLX(N, X) - Shift Left multiple: Shifts N left by X bits. Bits
shifted out are lost and zero bits are shifted in.
SRX(N, X) - Shift Right multiple: Shifts N right by X bits. Bits shifted out are lost and zero bits are shifted in.
RLX(N, X) - Rotate Left multiple: Rotates N left by X bits. Bits rotated off one end are rotated in to the other end
RRX(N, X) - Rotate Right multiple: Rotates N right by X bits. Bits rotated off one end are rotated in to the other end
Bit Set and Clear operations. Bitnum (bit number) is 0-MS bit.
RBit(N, Bitnum) Read the bit at Bitnum, returns bit state 0 or
WBit(N, Bitnum, TrueorFalse) Write the bit at Bitnum without disturbing other bits.True writes 1, False writes 0.
TBit(N, Bitnum) Toggle the state of the bit at Bitnum without changing other bits.
Matrix Sequencers, Table Sequencers, and File Sequencers can be read from and written to by the compiler. File and Table sequencers will accurately show the results of a step cell that has been changed, but only when being stepped into the cell; if they are already pointing to the modified cell, they will not update until the step changes. Matrix sequencers, because of the knob graphics, do get notified when a cell or cells are changed, and they will automatically update their graphics and output.
Matrix sequencers are addressed by Row, Column; Table and File sequencers only by Step. Matrix and File sequencer functions require a module ID ("mID") number to identify which module to access. This value is the same as the module number less 1. That is, M4x4-1 would have an ID of 0, M4x4-2 would be ID 1, etc.
R, W, and F are prefixes for the function to Read, Write, or return the "Frozen" status of a Matrix Sequencer running a CA or Life algorithm, so for example, to read from a 4x4 Matrix Sequencer, the mnemonic would be R4x4(mID, row, column), to write to it: W4x4(mID, row, column, value), and to read the Frozen flag: F4x4(mID).
Table sequencers are addressed by Page, Step. Both are numbered 0-127. There is only one Table, addressed by all Table sequencers, so the is no need to give it a mID number. To read from a Table: RTbl(Page, Step), and to write to a Table: WTbl(Page, Step, Value).
File sequencers only have a read/write step address and it can be any size, whatever the file size is. The file data size is a byte, 0-255, not 0-127, although the File Sequencer only reads the lower 7 bits (0-127). To read from a File Sequencer: RFile(mID, Step), to write to a File Sequencer: WFile(mID, Step, Value), and to get the file size (last legal Step address): FFile(mID). Note that you can use a File sequencer simply to read, write or create a sequence file data set for your own User Functions, and not otherwise used as a SoftStep module - it works as a UF-Prog storage module even when the module is Disabled as a SoftStep module.
The functions in this group are different from the other functions in one important way. While all the other functions are compiled into a single Function, seen by the module as the Fn# it calls, these functions can themselves call other Fn#s. With these functions you can write complete programs with testing, branching, looping and, of course, crashing.
The key concept to understand is that within the function compiler, there is an enclosed world of functions evaluating other functions, Sub Functions, variables and literals - but always the end result is an evaluation of a function, whether simple or complex. This gives you a very flexible calculator, but not a programming language.
However with this group of program flow control functions, other Fn#'s may be executed by the SoftStep module's Fn#. This gives true programming access to the function compiler. When you see a parameter in this group named "FNum" it refers to a Fn# that will be run as if a UF module were running it directly.
ME - While FNum must always be a number between 0 and 127 that represents the Uf-Prog Fn#, you do not have to use absolute numbers, and in most cases you should not. Instead, use the special variable ME - which is always the Fn# line the code is running - for relative addressing. Then instead of typing in line numbers, and having to retype them again if you move the code around, you can give addresses such as ME+1 (for the next line below) or ME-2 (for two lines above).
Ex( FNum ) - execute the given Fn#.
Switch( N , FNumT , [FNumF] ) - Tests N (which is just a normal parameter, not a Fn#), if N is True (non zero) then the first Fn# is executed, if N is False (0) then the 2nd Fn# is executed. Only one of the two Fn#'s will be run, unlike the IFF() function which evaluates both the True and False parts. Fn# 0 is a special case that allows single branch as well as double branch statements: if you use Fn# 0 for either FNumT or FNumF, then that branch is not taken. The last parameter, FNumF, is optional; if not given it defaults to 0.
Case( N , FNum , FNum , FNum , .... ) - Any number of F#'s can be entered (up to 30), separated by commas. Only the Nth Fn# is executed, all the others are ignored. N must be between 1 and the number of Fn#'s in the function.
List( FNum , FNum , FNum , ...) - Any number of F#s can be entered, up to 31. All are executed and the result of the last one is returned.
Loop( N , FNum , FNum , FNum , .... ) - Execute all Fn# given (up to 30) N number of times. N is zero based, so if N=0 the Fn# are executed once; if N=3 they are executed 4 times.
Ix - is a read only variable that is the list index for Loop() and List(). It is always the 0 based FNum index of the currently running function. Thus, when the first FNum in the list is executing, Ix = 0; when the 2nd FNum runs, Ix = 1, etc.
Jx - is a read only variable that is the loop count (N) for Loop(). It is always the 0 based count of the currently running loop function, making it possible to run loops within loops, and still have access to the inner loop indexes. This gives the equivalent of For...Next loop ability.
GoTo( FNum ) - This does not actually execute the given Fn#. Rather, it instructs the UF module to change its Fn#, so the next cycle the UF module will start executing the Fn#. The module stays on that Fn# until changed either by another GoTo, or manually by selecting the Fn# from the blue drop down menu. The Fn# drop down menu does not change to the GoTo FNum, it stays on the original FNum. But the tool tip on the module's caption label will change to the function label of the new FNum.
SnapShot does not record the GoTo Fn#'s. So if you take a SnapShot, or save the file, it is the original, pre-GoTo Fn# that is saved and restored. The GoTo Fn# must be within the module Fn# range of 0-127. The UFX module does not respond to GoTo because its Fn# is controlled by a value input, not a drop down menu.
Like the Get/Put memory array, there are up to 127 clocks, automatically assigned on an as-needed basis to any UF module that accesses one. These clocks are very efficient and add no additional burden to the multimedia timer load. They are nearly identical in function to the SoftStep Clock module and they are synchronized with the other SoftStep clocks.
TSet( Duration , PerCentOn ) - Clock set up. The Duration parameter is the same as used throughout SoftStep and documented in the Clock module section. PerCentOn is the clocks duty cycle. For example, a PerCentOn of 33 has the clock On 1/3 of the cycle and Off 2/3. The return value for TSet() is the Duration number of ticks, not the Duration parameter itself. For example, if you give a Duration of 0, then just as the Clock module, it will set to 12 ticks, or an eighth note; and the function will return 12.
Tick( FNumOn , FNumOff ) When the clock tick goes on, the first Fn# is executed; when it goes off the 2nd Fn# is executed. Fn# range is 1-127, with Fn# 0 being a special case that tells the clock to ignore it. Thus, you can have only one Fn# run if you like, either on Clock On or Clock Off, by giving the one you don't want to run an Fn# of 0. Clocks must be set up with TSet() before they will run.
Clk (no parentheses) is a special variable that returns True (1) when the clock is On, and False (0) when the clock is Off. Clocks must be set up with TSet() before they will run.
MIDITick (no parentheses) returns the MIDI tick rate in milliseconds for the current Tempo.
RunStop(N) - gives program control of the system Run/Stop flag. It is exactly the same as if the toolbar Run button were clicked with a mouse. Setting N to False (0) sets Run to False, Stop to True, and pops out the toolbar Run/Stop button. Setting N to True (any non zero) sets Run to True, Stop to False, and pushes in the Run/Stop button.
AutoStart(N) - sets clocked UF modules to automatically clock one cycle when the system Run/Stop flag triggers. Set N=1 (default) to enable, N=0 to inhibit. This is most useful when triggering a clocked UF module with a Strobe button - by inhibiting AutoStart, you will not get an unwanted triggers from the system Run/Stop flag.
SetAbsClkA(Msec) & SetAbsClkB(Msec) - Set the Abs Clk loop clock time. Timing values are in milliseconds (1000 msc = 1 second). Although you may set these clocks to any value from 0 to 32767, low (fast) settings will be limited by the computer speed and the complexity of the modules. Set the modules to run from the Abs Clk loop with the module's Priority pop up.
Life(Rows, Columns) is John Conway's Life algorithm played out in the LED Matrix array memory. Rows and Columns specify the matrix size and may be up to 32x32. Each time Life() is called it produces one Life generation, using the bits currently in the module's associated LED matrix array as the seed generation, and replacing it with the new generation.
Typically, you would use Life with a ULM module, as that is the one that can display the Life cells on the LED matrix; but you are free to use any of the UF modules, and access the cells with the LM functions.
CA(NumBits, Rule, Value) is a generalized 1-dimensional cellular automata algorithm, that can be displayed on the LED matrix or used in other ways. NumBits is the number of bits (columns) to use, and may be up to 32. Rule sets the manner in which the next generation will be produced. This is a number from 0-255 and is usually expressed in binary, such as #b0101101. It represents the pattern matches that will produce live cells (bits on) in the next generation. Value is the integer value to be operated on. It serves as the seed for the next generation. For detailed information on both CA and Life algorithms, see the Matrix Sequencer section. This algorithm is best when recursively applied. That is, when the value returned for each iteration is used as the Value parameter for the next.
These MIDI functions expand on the standard MIDI modules:
MIDICtrl(Port, Channel, CtrlNum, Value) - Sends a MIDI Control message to the Port (0-4), MIDI Channle (0-15) and Control number (0-127) selected. The value sent must be in the MIDI range of 0-127. This function sends a MIDI control message every time it is called, so care should be taken that it is not put into a module without some means to avoid MIDI data clogging.
MIDICtrlIn(Port, Channel, CtrlNum) - Returns the value sent by a MIDI Control message to the Port (0-4), MIDI Channle (0-15) and Control number (0-127) selected. Returns last value received, or 0 if no value has been received since startup.
MIDIPoly(Port, Channel, KeyNum, Value) - Sends a MIDI Poly Key Pressure message to the Port (0-4), MIDI Channle (0-15) and Key number (0-127) selected. The value sent must be in the MIDI range of 0-127. This function sends a MIDI Poly message every time it is called, so care should be taken that it is not put into a module without some means to avoid MIDI data clogging.
MIDIPolyIn(Port, Channel, KeyNum) - Returns the value sent by a MIDI Poly Key Pressure message to the Port (0-4), MIDI Channle (0-15) and Key number (0-127) selected. Returns last value received, or 0 if no value has been received since startup.
MIDIKeysOff(Port, Channel) - Clears sent flags for the MIDI Key Number and Key Velocity input modules, forcing these modules to read the next Keyboard input values whether or not a MIDI Note Off message has been sent.
NRPN(Port, Channel, ParamMSB, PramLSB, DataMSB, [DataLSB=9999], [RPN-Flag=0]) - Send NRPN (Non Registered Parameter Number) to the Port and Channel given. NRPNs conisist of 4 control messages with two bytes for the parameter ID and two bytes for the value. This gives a larger value range than the normal MIDI range of 0-127; the NRPN value range is -4096 to 4095.
Some devices use NRPNs to send the data as 2 bytes, and some as a single combined word. SoftStep allows you to have it either way: If you put 9999 into either of the MSB parameters, then only the value in the LSB parameter is sent, with a range of -4096 to 4095.
Also, many devices do not use the LSB data parameter at all, only the MSB data in the 0-127 range. Therefore if you put 9999 into the LSB parameter, assuming the MSB is in the 0-127 range, the LSB is not sent. The Data LSB is an optional parameter, if it is not used, it defaults to 9999, which causes only the MSB (0-127) to be sent.
MIDI also supports a few RPNs (Registered Parameter Numbers); you may send RPNs instead of NRPNs by setting the RPN-Flag parameter to 1 (any non zero). This is an optional parameter that defaults to 0: If you don't use it, NRPNs will be sent.
NRPNIn(Port, Channel, ParamMSB, ParamLSB, [RPN-Flag=0], [EraseAfterRead=0]) - Return last NRPN sent. if optional RPN flag is non zero, returns the RPN instead of NRPN. Because this function takes extra computer resources, it is not initialized until the first time it is called, therefore you should call it once during setup, discarding the result, if you plan to use it. This function always returns a signed integer between -4096 and 4096. To convert this to MSB & LSB, use the following formula:
MSB = (InValue + 8192) / 128
LSB = (InValue + 8192) Mod 128
If the optional parameter EraseAfterRead is non zero, the input values in the internal table that holds them will be set to 0 after it is read. This is useful for reading NRPN values that send increment and decrement signals instead of absolute values.
OpenCOM(COM, baud, "parity", bits, stop) - Open serial Com (1-4) port. All params optional. Defaults: COM=1, baud=9600, parity="N", bits=8, stop=1. Up to 4 Com ports, COM1 - COM4, are available but only one COM# can be open at a time. If you have trouble opening a COM port, it probably is in use by another program.
CloseCOM(COM) - Close serial COM(1-4) port. Default COM=1.
SendCOM(COM, "Any Text") - Send text string to COM (1-4) output.
GetCOM(TextVar, COM) - read serial COM (1-4) input, putting it into named string variable.
The title bar label of UF modules automatically receive whatever Fn# label name they are running as a general tool tip, but you can also set tool tips for each of the In1 - In4 inputs and the InC clock input with the type-in (not on the keyboard) function:
ToolTip("Clock tip", "In1 tip", "In2 tip", "In3 tip", "In4 tip")
All of the parameters must be strings, inclosed in quotation marks ("whatever you want"). All parameters are optional, but unused parameters comming before parameters you use must have an empty comma to mark their position. For example if you only want to give a tool tip for In1 and In2:
ToolTip( , "Tip for In1" , "Tip for In2")
Note the comma at the start, which marks where the unused Clock tool tip would be. When not specified the tool tips default to: "Strobe" for the clock and "In1" through "In4."
NumLabel(N) - displays the numberic value N in a UF module label.
TxtLabel("Text") - displays the text value "Text" in a UF module label.
The .ssp file "ufdemo.ssp" & companion "ufdemo.ufn" contain example functions and modules that will be described in this section. You should load it (ufdemo.ufn will automatically load into the programmer when you load ufdemo.ssp), to follow along as you read these examples.
Starting in the upper left hand corner of the SoftStep main screen, the
UF2 and UFO
modules are the same as covered in the Quick Start
Tutorial. UF2 has two value input knobs, and it is programmed to return the
MIN of the two by the line in Fn#1:
OUT( MIN( A.( IN1 ) , B.( IN2 ) ) ).
When you read this programmable calculator type script, remember that
the inner parenthesis statements are evaluated first. statements at the same
level are evaluated left to right. Here is the complete breakdown of the above
A.( IN1 ) - variable A gets the value of module input1
B.( IN2 ) - variable B gets the value of module input2
MIN( A.( IN1 ) , B.( IN2 ) ) - calculate the minimum of the above two values
OUT( MIN( A.( IN1 ) , B.( IN2 ) ) ) - send the result to the module's output.
This example actually uses two UF modules, the UF2 set to Fn#1 to return
the MIN of the two Value knobs, and the UFO set to Fn#2 to return the MAX, and
to light the LED indicator when they are equal:
OUT( MAX( A , B ) ) , LED( A= B).
Here, you have two statements at the top level, separated by a comma.
They are separate program statements and are evaluated left to right. There
usually is some logical connections to the separate statements, but there is no
requirement for it. So the above Fn line can be seen as: OUT( MAX( A , B )
) for the first statement, and LED( A= B) for the second. Here is
MAX( A , B ) gets evaluated first because it is in the inner parentheses of the first statement. Module UF2, discussed above, has already set variables A and B with the values of the Value knobs that are connected to it.
OUT( MAX( A , B ) ) sends the result of the above MAX function to the UFO output.
A= B compares A with B, and returns True (1) if they are equal or False (0) if they are different. The "=" is an operator, which is essentially a function with a left part and a right part, and no enclosing parentheses.
LED( A= B) sends the result of the compare (0 or 1) to the LED() function, which turns on module's LEDS according to the "On" bits. Since there is only 1 LED in the default UFO, this has the effect of turning on the LED when the two variables are equal.
The next two UFO modules, UFO-02 and UFO-03 show two different ways of using the UF internal clock. Both modules start with a setup line that initializes the clock the module uses, then sends the module on to the next Fn#, where it will "live." That is, the module will stay on the last Fn# that it was sent to by a GoTo() Function. The drop down Fn# remains set to the first Fn# the module will execute upon startup or reset (as on file load, when the Run button is clicked in), but you can tell it is actually running a different Fn# by moving the mouse cursor to the module label, which will read as its tool tip the name of the Fn# it is currently running.
The UFO-02 module is using the Clk statement, which is
simply a special variable that returns 1 when the clock is "On" and 0 when the
clock is "Off." But before using the clock it must be set up by
TSET( DURATION , PERCENTON ) , GOTO(ME+1)
There are two separate statements, the first to set up the clock and the second to jump to the next line. Note that DURATION and PERCENTON have been predefined in the right hand Variables section. For simplicity these two variables are used for all the clocks in all the examples - but normally you would assign each clock its own values for percent on and on duration.
The special variable ME is always the line it appears on. Using ME+1 allows you to jump to the next line without having to actually give a hard coded line number, so you can move the lines of code around and as long as they are positioned the same relative to each other, everything works.
The UFO-03 modules is using the Tick() function, which
causes separate Fn#'s to execute on the On and Off tick:
ME+1 which is executed at the start of the On part of the clock tick is: OUT( 127 ) , LED( 1) - which simply sends 127 to the module's output, and turns the LED on. ME+2, which is executed on the Off part: OUT( 0) , LED( 0) - sets the output to 0 and turns off the LED.
There is a subtle but important difference between how the GOTO() function used in the setup line works and the Tick() function's Fn# execution. In the case of the GOTO() function, the module running the function is told to change its Fn# pointer, so the module actually changes its setting. In the case of the TICK() execution of other Fn#s, the Fn# is executed but the module is not told to change its Fn# pointer, and in fact does not know that it is executing more than one Fn#. This is true for all the control statements that execute other Fn#s, with the single exception of the GOTO() function.
The ULM module on the left, ULM-01 with the 32x32 LED matrix running Life, is not very useful musically, but it illustrates several UF programming concepts. Besides, it looks cool.
The first line, Fn#12 is the initialization for the clock and
NLED(-32), NLED(32), TSET(DURATION, PERCENTON) , GOTO(ME+1)
The LED matrix is set up to be 32x32, the clock is initialized, and it
jumps to the next line:
LED( LMRAN) , GOTO(ME+1)
...which initializes the LED matrix to random bits (LMRAN) and displays them, then goes to the next line. This line is separate from the above initialization line because it is used later in the program again when the Life algorithm goes static and needs to be restarted.
The next line, Fn#14 (which the module remains set to) is the
TICK( ME+1, 0). Note that the Off Fn# is set to 0. This is a special case, which is not executed. That is, when either of the ticks of the TICK() function is set to 0, nothing happens on that part of the tick.
At the On tick, Fn#15 is executed. This is the main body of the Life
LIFE(32, 32), LED( V0.(OUT(WRAP(V0+1)))) , SWITCH( TEST_CRC() , ME-2, 0)
Notice there are three separate functions here. Taken one at a time, they are:
LIFE(32, 32) which runs the Life algorithm on the data that currently exists in the LED Matrix memory associated with the module that is running it, in this case the ULM-01. This does not display the results, it only runs the algorithm and leaves the memory array set to the new generation.
LED( V0.(OUT(WRAP(V0+1)))) This causes the led matrix to display
the new results, but it does more than that. Taking it from the inner
V0+1 simply adds 1 to the private variable V0. No magic there. V0 is used because it is private to the module, like the LED matrix memory and the clocks. The purpose of V0 is simply to give a changing value to the LED function, which requires a new value in order to display the LED matrix. Like many functions in SoftStep, LEDs do not display the same values over and over as it would use up CPU cycles unnecessarily, so it has to be fed a fresh value each time you want it to redisplay the matrix.
WRAP(V0+1) causes the incremented V0 to roll over at 127, so it counts 0-127 and repeats. Out() also limits its value to the legal MIDI range of 0-127, but it does not wrap. Instead it forces anything below 0 to 0 and anything above 127 to 127. So without the WRAP() function, the value would go up to 127 and then stay there as V0 continues to increment and Out clamps it to 127.
OUT(WRAP(V0+1)) sends the wrapped increment of V0 to the module output.
V0.(OUT(WRAP(V0+1))) saves the new V0.
LED( V0.(OUT(WRAP(V0+1)))) takes what is now a new value (last time LED looked, it was V0, now it's V0+1), which is a signal to display the LED Memory matrix onto the LED blinky lights.
SWITCH( TEST_CRC() , ME-2, 0) which is the third and last statement for the line, tests to see if the Life algorithm has quit producing new generations, and if it has it kick starts it with a random display. The SWITCH() function works like the TICK() function, but instead of testing a clock tick for on/off, it tests whatever you give it as the 1st parameter. As always, 0 is False and anything else is True. So the SWITCH() is testing the results of function TEST_CRC(), and if True, it runs line ME-2, which is the 2nd initialization line (Fn#13), which reseeds the LED Matrix memory with random bits. If TEST_CRC() returns False, then nothing is done - like with the TICK() function, setting one of its pointers to 0 tells it to do nothing in that instance.
TEST_CRC() is a Sub Function, defined in the lower pane of the UF
INS(LMCRC ) , (GET(0) = GET(1) ) OR (GET(0) = GET(2) ).
The first statement:
INS(LMCRC ) gets the CRC of the LED Matrix memory. The actual value of the CRC is unimportant. What matters is that it changes if there was a change in the LED Matrix, and it returns the same value if there is no change. The INS() function inserts this value into the 0th element of the 16 element private memory array, and moves the other values down one. This sets element 0 as the most recent CRC, element 1 as the previous CRC, element 2 as the CRC before that, and so on.
(GET(0) = GET(1) ) OR (GET(0) = GET(2) ), the second statement of this Sub Function, tests to see if either the last CRC is the same as the current one, or if the one before last is the same as the current one. This handles the case where the Life generation has gone static, and also where it has gone into oscillation between two states.
The ULM-02 module is similar to ULM-01, but instead of running
Life on the matrix, it is running CA (1D cellular automata). The first line,
Fn#24, is again an initialization line, that sets up the module to a
32x32 LED matrix, sets the clock, and sets up the matrix memory to run the CA
NLED(-32), NLED(32), TSET(DURATION, PERCENTON) , LMCLR, LM.(0, V0.(LRAN) ) , GOTO(ME+1)
There are four statements in this line, but since the LED setup, clock
setup and GOTO() statements have been covered already, we will only look at the
two statements that set up the CA algorithm:
LMCLR is clears the LED Matrix memory to 0.
LM.(0, V0.(LRAN) ) simply gets a random bitfield (LRAN) to be used as a startup seed, and puts it into the private variable V0 as well as the 0th element of the LED Matrix memory array.
The next line (Fn#25) is the same clock tick used before that
executes the line following on the start of the On clock tick. The final line
of this group (Fn#26) actually runs the CA algorithm:
V0.(CA( 32 , RULE , V0 )), LMI( V0 ) , LED( V0) , OUT(WRAP(V0)).
Four statements in this line:
V0.(CA( 32 , RULE , V0 )) runs one iteration of the CA algorithm and puts the results in V0.
LMI( V0 ) inserts the new CA generation into the 0th element of the LED Matrix memory array, first moving all the other elements down one, and discarding the old 31st element. The point of this is to show the CA results in the top line of the LED matrix, and the earlier generations on the lines below, so it has the effect of cascading down from the top.
LED( V0) tells the UFO-01 module to display the updated LED memory on its LED Matrix.
OUT(WRAP(V0)) sends the results of the CA generation to the module's output, as a semi random number with a lot of 0's. The excess 0s are due to an anomaly (that's a bug that turns out to be useful) that treats bits in the 31st position as negative numbers, which are translated to 0.
The ULR module, displaying a single row of 32 LEDs is also running the CA algorithm, but it is set up to run CA in a way that is more likely to be useful in a musical context. In this case, the CA is run then each of the 32 CA cells is sent to the output as a rhythm line. After all 32 cells are sent to the output, the next generation CA is generated, and so on.
The first line (Fn#18) is the initialization, and the 2nd line
(Fn#19) is the Tick() function, both of which has been covered, so we
will start with the 3rd line (Fn#20):
OUT(RBIT( V0 , V1) ) , SWITCH( V1.(V1 +1) >= 32 , ME+2, 0) .
OUT(RBIT( V0 , V1) ), the first statement reads the bit in VO
(which holds the CA generation) at bit position V1, which is used as an index
to count through the 32 bits (CA cells).
SWITCH( V1.(V1 +1) >= 32 , ME+2, 0) handles both the indexing and it calls for a new generation at the end of the count. The test part of the SWITCH() function, the first parameter, does the heavy lifting:
V1.(V1 +1) >= 32 increments V1, keeping the new value. The comparison operator >= tests if the count has gone above 31. This is the result SWITCH() sees to determine whether to execute ME+2 (which generates a new generation in V0 and displays it in the module's LEDs), or 0 (do nothing because it has not reached the end of the bit row).
The UFV module next to the
Image module, draws a sine wave on the Image
module. The first line (Fn#28) initializes the module to have 3
NLED(3) , GOTO(ME+1).
UFV and UCV modules do not have LEDs, but they do have a variable number of inputs; so the NLED() function is used to set the number of inputs. Three inputs are used, each connected to a Value knob. The inputs control the sine wave period (number of peaks and valleys), the amplitude, and the offset, in that order.
The line that does the work (Fn#29) is composed of 5 statements. We will
examine them one at a time:
X.(WRAP(X+1)), SWITCH( X , 0, ME+1) ,Y.((-SIN(X/((IN1/4) +0.1)) *IN2) - IN3 ) , WPIX( MID , X , Y, C ) , OUT( ABS(Y ))
X.(WRAP(X+1)) increments the X parameter. This will go across the image from 0 to 127.
SWITCH( X , 0, ME+1) tests X for 0, the start of the sine wave, erasing the previous image before drawing the next.
Y.((-SIN(X/((IN1/4) +0.1)) *IN2) - IN3 ) calculates the Y
parameter, which is the part that goes up and down. Breaking this down from
inner parenthesis outward, we get:
X/((IN1/4) +0.1) divides X by IN1 (input 1), which is itself reduced by 1/4. This allows IN1 to control the period of the sine. The +0.1 protects from dividing X by 0, which would generate an error.
-SIN(X/((IN1/4) +0.1)) *IN2 creates a Y that is a sine function of X, the horizontal step index. IN2 scales the function with a simple multiply.
(-SIN(X/((IN1/4) +0.1)) *IN2) - IN3 adds IN3 (by subtracting from the negative sine value) to give an adjustable offset control.
Y.((-SIN(X/((IN1/4) +0.1)) *IN2) - IN3 ) Stores the final Y result in variable Y.
WPIX( MID , X , Y, C ) writes the pixel to the Image module. Note that the values for MID (the Image module's ID, and C (the pixel color) are preset in the Variables box. The MID value is always the module number ("1" in "Image-1") minus 1. Also notice that even though C is a predefined variable, it can be redefined in the Variables box.
OUT( ABS(Y )) sends the absolute value of the sine function to the module output.
SoftStep's User Function compiler is an extended implementation of Daniel Corbier's Fast Math Parser. You can read more about this remarkable software component, including a detailed programmer's manual, by visiting his web site at: http://www.ucalc.com.
Being able to have program-like user functions, such as testing, looping, goto, etc., gives a great deal of flexibility, but it also carries a price: you can create runaway programs that lock up the system and crash. There are two kinds of crashes you are likely to encounter. One is due to numerical errors that result in an Overflow error, and the other is due to a runaway loop.
Runaway loop lock ups happen because SoftStep runs in a tightly controlled loop, and the UF modules can be inserted into any part of that loop (they have the same priority buttons as the other modules, and are treated no differently than other modules). While it would be a simple thing to insert an "escape hatch" into the UF program functions to prevent runaway loops, doing so would cause a serious timing issue.
This "escape hatch" is a simple call to Windows, to see if other events need processing. All Windows programs need these check points in their program to allow Windows to multi task. With SoftStep they occur after the modules have all executed, during the time they wait for the next MIDI clock. By not inserting the checks within UF program functions, you are practically guaranteed you will have crashes and lock ups while you are debugging your programs - the trade off is that once you get the program to work, it will work cleanly within SoftStep's tight music timing environment.
Numeric errors, which almost always will be Overflow errors, are also a safety vs. performance issue. Most numerical errors are in fact trapped, and you will encounter numerous error messages when you enter your program scripts. During UF module execution, most numerical errors are also trapped, usually to just return 0 if there is a problem and go on. But there is one type of possible error that often is not trapped. This is the overflow error you may encounter when functions convert between floating point and integers.
The numeric type conversion itself is automatic and you do not need to track it; but during conversion, the double precision floating point value, which is the natural format for numbers within the compiler, may exceed the magnitude of the integer it is being converted to, and thus generate an overflow error. Most of these are trapped, and you will only see them as errors or anomalies in the function, but in some cases putting trapping code into a function that is not likely to have an overflow becomes an undesirable performance trade off.
You can avoid overflow problems simply by not assuming you can insert any number into any function and all will be well. For example if you give a negative number to a function parameter where negative numbers make no sense (such as the color values in RGB) you are likely as not to get an overflow error.
The good news is that once you have your UF module running and debugged, you can expect it to run thereafter without trouble. And because of the above performance trade offs (and because user functions are pre compiled), there is very little performance penalty in using the UF modules you have designed.
SoftStep is Copyright © 1999-2004 by John Dunn and Algorithmic Arts. All Rights Reserved.