Follow

PAL Scripting Quick Start Guide

Overview:

PAL is short for Playlist Automation Language and was developed to give station owners full power over their music rotation.  The goal was that for anything a station playlist programmer could dream up - PAL would be able to achieve it.  Today thousands of SAM Broadcaster users use the power of PAL to completely automate major functions of their station, including switching DJ's, running two-for-Tuesday, hourly news, precise scheduled jingles and advertisements, and even downloading content into their rotation.

This guide aims to quickly teach you the basics of PAL scripting and provide enough real-world examples to get you started.  At first PAL scripting might seem very daunting to any new user, but just stick with it for a while and you will quickly realize the power you can wield in automating your daily tasks.  It is certainly worth the sweat and tears after you have overcome the initial learning curve.

How to create a PAL script:

PAL Scripting - "Hello World" Example

Step 1. In SAM Broadcaster go to the menu option Menu->Window->PAL scripts and activate the PAL script window.

Step 2. Click on the Add ("+") button. This will open the script options dialog.

Step 3. Next to the script source file, click on browse ("folder") button located to the right of the edit field. Enter the name of the script you want to load, or, if the script does not exist, the name of the script to create. For this example, it is "Guide.PAL". Click OK. (If prompted to create the file, select yes...)

Step 4. The Guide.PAL script entry will appear inside the PAL Scripts window. Double-click on the Guide.PAL entry to open up the PAL IDE.

Step 5. Inside the PAL IDE type the following text:


WriteLn('Hello world!');

 

Step 6. Click on the Run button (or press [F9] ).

The BLUE line shows which line is being executed, and the output is written on the right hand side of the PAL IDE. (It will show "Hello world!".) The status of the script in the status bar will also be displayed in the PAL IDE.
Congratulations!  You have just entered the awesome world of radio automation scripting!   

The PAL scripting language was based on PASCAL, to be more exact, the OOP version of Pascal called Delphi.  Delphi is the powerful object orientated programming language that was used to develop SAM Broadcaster. Thus it was only fitting to base PAL around this powerful language.

The basic structure of this language will be explored in the sections to follow and will form the basic building blocks of our more advanced PAL scripts.  Note: Before we continue here you must already know how to create a blank PAL script. See the "PAL scripting: Hello world example" above.

Comments

Comments are text added to a script that is totally ignored by the compiler. This is used to provide more information to any human script viewer to make it easier to read and understand the logic of the script.

PAL supports two different comment styles:


{ This is a comment }
// This is another comment

Output Window

While developing PAL scripts, you will find the PAL IDE very useful. It shows you the exact line that is currently executing. However, many times you need to know what is going on in the actual script. Since the PAL IDE does not support variable inspection you should use the age-old technique of writing the current state of affairs to the output window.

There are two simple commands for doing this:


WriteLn(Value);
WriteStr(Value);

 

The only difference between the two is that the WriteLn command will append a newline character at the end of the string, causing the string to wrap into a new line in the output window.

Compare:


var T : Integer;
for T := 1 to 3  do
       WriteLn('WriteLn '+IntToStr(T));

 

with


for T := 1 to 3 do
        WriteStr('WriteStr '+IntToStr(T));

 

Variables & Constants

Variables and constants are used to give a name to a certain data value.  Constant values can not change during the operation of the script.  Variables however can be changed at any time during the script.  Unlike most languages, the name of the constant or variable is not case sensitive. Thus to PAL MyVar and MYvar are the same thing.

Constant values are declared as:


const MyConstValue = ConstValue;

 

Examples:


const Val1 = 'This is a string value';
const Val2 = 1234;
const Val3 = 1.234;
const Val4 = false;

 

Varibles are declared as:


var MyVarName : vartype = default value;

 

Examples:


var Val1 : String = 'This is a string value';
var Val2 : Integer = 1234;
var Val3 : Float = 1.234;
var Val4 : String;

 

As you can see, a default value is not required for variables, but it is recommended you use one where possible.  The PAL language supports all major variable types like string, integer, float, Boolean, variant and datetime.  Variables can also be arrays and objects.   Variables can get a new value during the life of a script. Let’s look at this example:


var S : Integer = 0;
PAL.Loop := True;

WriteLn(S);
S := S + 1;

WriteLn(S);
S := 123;

WriteLn(S);
PAL.WaitForTime('+00:00:10');

In the example above, the initial value of S is zero (0), and then the value is set to S + 1, which is 1+1 = 2.  Finally, S is set to 123, and then we wait for 10 seconds before the script is restarted.

Each time the script restarts, the variables are cleared. Thus S will have the value of 0 again once the script restarts.

Math

Basic integer math


var m : integer;
{Note: a & b below are both of type integer}
m := a + b;
m := a - b;
m := a * b;
m := a div b; {Integer only; Discards remainder}
m := a mod b; Integer only; Only remainder}
m := -m;

 

Basic float math


var m : float;
{Note: a & b below can be either of type integer or float}
m := a + b;
m := a - b;
m := a * b;
m := a / b; {Float only}
m := -m;

 

Other Available Math Functions


m := Round(1.2345); {Rounds value to closest integer value)
m := Random; {Returns random floating point value}

function Sin(a : Float):Float;
function Sinh(a : Float):Float;
function Cos(a : Float):Float;
function Cosh(a : Float):Float;
function Tan(a : Float):Float;
function Tanh(a : Float):Float;
function ArcSin(a : Float):Float;
function ArcSinh(a : Float):Float;
function ArcCos(a : Float):Float;
function ArcCosh(a : Float):Float;
function ArcTan(a : Float):Float;
function ArcTanh(a : Float):Float;
function Cotan(a : Float):Float;
function Hypot(x : Float; y : Float):Float;
function Inc(var a : Integer; b : Integer):Float;
function Abs(a : Float):Float;
function Exp(a : Float):Float;
function Ln(a : Float):Float;
function Log2(a : Float):Float;
function Log10(a : Float):Float;
function LogN(n : Float; x : Float):Float;
function Sqrt(v : Float):Float;
function Sqr(v : Float):Float;
function Int(v : Float):Float;
function Frac(v : Float):Float;
function Trunc(v : Float):Float;
function Round(v : Float):Float;
function Power(base : Float; exponent : Float):Float;
function DegToRad(v : Float):Float;
function RadToDeg(v : Float):Float;
function Max(v1 : Float; v2 : Float):Float;
function Min(v1 : Float; v2 : Float):Float;
function Pi:Float;
function Random:Float;
function RandomInt(Range : Integer):Integer;
procedure Randomize;
function RandG(mean : Float; stdDev : Float):Float;
function RandSeed:Integer;
function SetRandSeed(Seed:Integer);

 

String handling

Detailed string handling is a bit beyond the scope of this document, but we will introduce a few basic techniques quickly.


Var S1 : String = 'This is my first string';
Var S2 : String = 'Another string';
Var I : Integer;
S2 := S1; {Copy a string}
S2 := S1 + ' and it rocks'; {Combine strings}
S2 := Copy(S1,2,3); {Copy only certain characters to a string. (Copy 3 characters, starting at position 2)
WriteLn(S2);
I := Length(S2); {Set I to the number of characters that S2 contains}
I := Pos('my',S1); {Find the index of the first match}

 

Other Available String Functions


function IntToStr(v : Integer):String;
function StrToInt(str : String):Integer;
function StrToIntDef(str : String; Def : Integer):Integer;
function IntToHex(v : Integer; Digits : Integer):Integer;
function FloatToStr(v : Float):String;
function StrToFloat(str : String):Float;
function StrToFloatDef(str : String; Def :Float):Float;
function Chr(x : Integer):String;
function Ord(s : String):String;
function CharAt(s : String; x : Integer):String;
procedure SetCharAt(var S : String; x : Integer; c : String);
procedure Delete(var S : String; Index : Integer; Len : Integer);
procedure Insert(src : String; var Dest : String; Index : Integer);
function LowerCase(Str : String):String;
function AnsiLowerCase(Str : String):String;
function UpperCase(Str : String):String;
function AnsiUpperCase(Str : String):String;
function Pos(SubStr : String; Str : string):Integer;
function Length(Str : String):Integer;
function TrimLeft(Str : String):String;
function TrimRight(Str : String):String;
function Trim(Str : String):String;
function CompareText(Str1 : String; Str2 : String):Integer;
function AnsiCompareText(Str1 : String; Str2 : String):Integer;
function CompareStr(Str1 : String; Str2 : String):Integer;
function AnsiCompareStr(Str1 : String; Str2 : String):Integer;
function IsDelimiter(delims : String; s : String; Index : Integer):Boolean;
function LastDelimiter(delims : String; s : String):Boolean;
function QuotedStr(Str : String):String;

 

Time handling

For radio stations time is usually very important. PAL contains a lot of features that helps programmers to deal with time.

The basic time variable type is DateTime


var D : DateTime;

 

With this format the date and time is represented as a floating point value.  The integral part of a DateTime value is the number of days that have passed since 12/30/1899. The fractional part of a DateTime value is fraction of a 24 hour day that has elapsed.

Following are some examples of DateTime values and their corresponding dates and times:

0      12/30/1899 12:00 am

2.75   1/1/1900 6:00 pm

-1.25  12/29/1899 6:00 am

35065  1/1/1996 12:00 am

Example statements:


D := Now; {Current date/time}
D := Date; {Today, with the fraction part 0}
D := Time; {The time only, the integral part is 0}
D := Now - 1; {Yesterday, this exact time}
D := Now + (1/24); {One hour from now}

 

PAL also has a very useful date mask function called TimeMask.

The TimeMask property is used to generate a DateTime value and can be in several formats:

Standard time

HH:MM:SS
Mask must be in 24 hour format and contain all fields (HH, MM and SS - where HH is hours, MM is minutes and SS is seconds)

Example

T['14:00:00'] would generate a DateTime value of exactly 2pm today.

Time calculation


+HH:MM:SS
-HH:MM:SS

 

Example:


T['+00:00:30'] // generates a time exactly 30 seconds from the current date and time.
T['-01:00:00'] // generates a time exactly 1 hour earlier.

 

Next Time (Wildcards)


XX:MM:SS
HH:XX:SS
HH:MM:XX 

 

XX acts as a wildcard, where the mask will try and find the next time that fits in the mask.
For example lets assume the current time is 2:05pm and we use XX:00:00 as our mask.  The resulting time will be 3:00pm because that’s the closest time in the future that fits the mask with wildcards.

Next Time (Fixed)

NEXTHOUR  
NEXT60  Both returns the next hour after the current time. (Same as using  XX:00:00)

NEXTHALFHOUR
NEXT30   Both returns the next half hour after the current time. 
For example if the current time is 2:15pm then using NEXT30 will result in 2:30pm.  If the time was 2:50pm, then using NEXT30 would return 3:00pm.


NEXTQUARTER
NEXT15 Both will return the next quarter after the current time.  For example using the NEXT15 mask at 2:05pm will return 2:15pm

TimeMask property example Writes the result of certain time masks to the output window.


WriteLn('--Standard time');
WriteLn(T['14:00:00']);                {## 2PM today}
WriteLn(T['04:30:00']);                {## 4:30AM today}
WriteLn(DateTime(T['09:00:00']-1));    {## 9AM YESTERDAY}
WriteLn(DateTime(T['09:00:00']+1));    {## 9AM TOMORROW}

WriteLn('--Calculation');
WriteLn(T['+01:00:00']);               {## One hour from now}
WriteLn(T['-00:00:30']);               {## 30seconds back}
WriteLn(T['NEXT60']);                {## Next hour}
WriteLn(T['NEXT30']);                {## Next halfhour}
WriteLn(T['NEXT15']);                 {## Next quarter}

WriteLn('--WildCards');
WriteLn(T['XX:00:00']);                {## Same as Next Hour}
WriteLn(T['XX:59:30']);                {## 30 seconds after next minute of the hour}
WriteLn(T['XX:XX:30']);                {## Next :30 seconds}

 

Examples:


D := T['13:00:00']; {1pm today}
D := T['+00:00:10']; {10 seconds from now}
D := 1 + T['+00:00:10']; {Tomorrow, 10 seconds from now}

 

Decoding the date into its multiple parts:


var y,m,d : Integer;
var hh,mm,ss,ms : Integer;
var D : DateTime;

D := Now;
DecodeDate(D,y,m,d);
DecodeTime(D,hh,mm,ss,ms);

DayOfWeek function:
if DayOfWeek(Now) = Sunday then {Do stuff};

 

The days of the week are stored in constant values you can use. Sunday = 1 to Saturday = 7.

Other Available DateTime Functions


function Now: DateTime;
function Date: DateTime;
function Time: DateTime;
function DateTimeToStr(dt: DateTime):string;
function StrToDateTime(str : String): DateTime;
function DateToStr(Date: DateTime):String;
function StrToDate(Str : String): DateTime;
function TimeToStr(Time : DateTime):String;
function StrToTime(Str : String): DateTime;
function DayOfWeek(dt : DateTime):Integer;
function FormatDateTime(Format : String; dt : DateTime):String;
function IsLeapYear(Year : Integer):Boolean;
function IncMonth(dt : DateTime; nm : Integer):DateTime;
procedure DecodeDate(dt : DateTime; var yy,mm,dd : Integer);
function EncodeDate(yy, mm, dd : Integer):DateTime;
procedure DecodeTime(dt : DateTime; var hh, mm, ss, ms : Integer);
function EncodeTime(hh, mm, ss, ms : Integer):DateTime;

 

Decision making & selection

PAL scripts can compare values and act upon these results. The basic operators are:

Operand Description
= Equal to
<> Not equal to
<= Less than or equal to
>= More than or equal to
NOT Logical NOT operator
AND Logical AND operator
OR Logical OR operator
{} Parenthesis used to logically group expressions

All of these are called logical or "Boolean" operators, since the final result is always TRUE or FALSE.

Example:


Var B : Boolean;
Var X : Integer = 0;
Var Y : Integer = 1;
Var Z : Integer = 2;

B := X<0; {B=FALSE}
B := X = 0; {B=TRUE}
B := Z>Y; {B=TRUE}
B := NOT(X > 0); {B=TRUE}
B := (X<0) OR (X=0); {B=TRUE since X=0}
B := (X<0) AND (X=0); {B=FALSE}
B := (X<=0); {B=TRUE}
B := NOT B; {Changes value from true to false, or false to true}
B := (X<>Z); {B=TRUE, since X is not equal to Z}
B := (1+1); {Invalid!! Not boolean result}

 

Logical expressions can be used in IF or CASE statements (see directly below), or REPEAT..UNTIL and WHILE..DO loops.

 Very important note: Due to implementation problems, you can not do PAL.WaitForXXX within an IF statement block.

Basic selection logic

The IF..THEN logic block is used to specify what sections of code need to be executed depending on a logic expression.

Example:


IF DayOfWeek(Now) = Sunday THEN
WriteLn('Today is Sunday!')
ELSE
WriteLn('Today is not Sunday...');

 

 


IF (Now>T['14:30:00') AND (DayOfWeek(Now)=Saturday) THEN
WriteLn('It is time for my Saturday live show!');

 

 


IF (Now>T['14:30:00') AND (DayOfWeek(Now)=Saturday) THEN
BEGIN
WriteLn('It is time for my Saturday live show!');
WriteLn('so lets get going...');
END;

 

Sometimes you have to check many values at the same time.  You can use nested IF statements, for example:


IF (A = 1) THEN
BEGIN
{Do  stuff}
{And some more}
END
ELSE IF (A = 2) THEN
{Do stuff}
ELSE
{Do other stuff};

 

But using CASE statements are usually a more elegant solution:


CASE A OF
1 : begin
{Do stuff}
        {And some more}
end;
  2 : {Do stuff};
else {Do other stuff}
END;

 

A needs to be an ordinal expression or a string expression.  Ordinal expressions are data values in a range, i.e. integer & Boolean are ordinal values, float is not.

Example:


var A : String = 'ABC';
CASE A OF
'ABC' : WriteLn('This is our ABCs');
'XYZ'  : WriteLn('This is our XYZs');
END;

 

Loops & Iteration

In programming one often find the need to repeat certain commands a number of times until a certain condition is met.  PAL scripts contain a few basic ways to achieve this.

a) Firstly, the script itself can repeat.

By setting


PAL.Loop := True;

 

the whole script will repeat once it has reached the end. If this is set to false during execution, the script will stop once it reaches the end, (False is the default value).

b) Using a 'For' loop


var T : Integer;
for T := 0 to 20 do
begin
WriteLn(T);
end;

 

c) Repeat .. until loop


T := 0;
repeat
WriteLn(T);
T := T + 1;
until (T > 10);

 


T:=33;
repeat
WriteLn('You will see this line exactly once!');
  T := T + 1;
until (T > 10);

 

d) While..do loop


T := 0;
while (T<20) do
begin
  WriteLn(T);
  T := T + 1;
end;

 

 
T:=33;
while (T<10) do
WriteLn('You will NEVER see this line!');
T := T + 1;
end;

 

Beware of infinite loops! Loops should always have some code that will cause it to eventually exit the loop.

These are examples of infinite loops:


T := 0;
while (T<10) do
begin
   WriteLn(T);
end;

 


repeat
T := 0;
  T := T + 1;
until (T>10);

 

Waiting....

Although technically not part of the PAL base language, it is crucial we cover the subject of waiting in this section since it is such a core part of most PAL scripts.  A PAL script executes one command or one block of commands every second. For most scripts this is too fast - they usually only need to take action once a song changes, or once a certain time is reached. Theoretically, we could just create a loop that will loop until the condition is met, but this will just waste CPU.

Thus in PAL we have many wait commands, often collectively referred to as "WaitForXXX" commands. (And no, we are not talking about slow porn downloads)

PAL supports 3 waiting commands:


PAL.WaitForPlayCount(Count);
PAL.WaitForQueue(Count);
PAL.WaitForTime(Time);
 

 

Using these commands will cause SAM Broadcaster to "pause" the PAL script until the specified event occurs.

Examples:

PAL.WaitForPlayCount(2);

This command will wait for exactly two songs to play before the script is resumed. This is done by setting a counter internally as soon as the WaitForPlayCount is executed, then for each song that is loaded into a deck and starts playing the counter are decreased. As soon as the counter reaches zero - the script is resumed.


PAL.WaitForQueue(2);

 

This command will wait for the queue to contain exactly two tracks. So if the queue had 4 tracks, and you start playing tracks from the queue, the script will eventually continue once the queue reaches 2 tracks. On the flip side, if the queue had zero tracks in it, this command will wait until 2 tracks are added to the queue.  Think carefully when you use this command - it can be tricky especially if you have multiple PAL scripts managing tracks in the queue. It is very possible that other scripts can keep the queue full (i.e. at 4 tracks) and that the queue will never reach 2 tracks.


PAL.WaitForTime(Time);

 

This is indeed a very useful wait command. First we need to cover the different ways this can be called. The "Time" can be specified in many ways.

a) As a DateTime variable


var D : DateTime;
D := Now + 1; {Tomorrow this time}
PAL.WaitForTime(D);

 

Another example - using the T timemask object we learned about earlier. This also returns a DateTime value.


PAL.WaitForTime(T['14:00:00']);

 

b) As a string time mask.

The exact same time mask format used with the T time mask object can be used in the WaitForTime command.

Example:


PAL.WaitForTime('+00:00:10');  {## Wait for 10 seconds}
PAL.WaitForTime('14:30:00');  {## Wait for 2:30pm TODAY}
PAL.WaitForTime('XX:00:00');  {## Wait for next hour}

PAL.WaitForTime('14:00:00'+1);  {## This is invalid! See below for correct syntax}

 

Use this instead:


PAL.WaitForTime(T['14:00:00']+1); {## This works - uses DateTime value}

 

So in short - you can either pass a DateTime variable or value, or pass a string time mask.

There is a subtle logical issue you need to be aware of as a PAL developer.

Say you have this PAL script:


PAL.Loop := True;
PAL.WaitForTime('10:00:00');
WriteLn('It is now 10am in the morning');
PAL.WaitForTime('20:00:00');
WriteLn('It is now 8pm in the afternoon');
PAL.WaitForTime('23:59:59');

 

If this script is started at 7am in the morning, then it will work as expected. SAM Broadcaster will wait till 10AM, write the message, wait till 8pm, write the message, and then wait till 1 second before midnight before the script is restarted - ready for the next day.

However, if this script is started at 5pm, then SAM Broadcaster will check the first PAL.WaitForTime('10:00'); and realize that 10am < 5pm, so it will not wait and immediately print out the message "It is now 10am in the morning". Then from this point onward it will work as expected - i.e. wait till 8pm, write the message, and then wait till 1 second before midnight before the script is restarted - ready for the next day.

More advanced PAL developers know this and write slightly more complex scripts to skip over sections of the day that already passed them by without executing the code.  We provide one quick example of how this can be achieved - although there are many other methods that may be used.


var OldTime : DateTime;
OldTime := Now; {Get the time the script started}

PAL.Loop := True;
PAL.WaitForTime('10:00:00');
if (OldTime<=T['10:00:00']) then WriteLn('It is now 10am in the morning');
PAL.WaitForTime('20:00:00');
if (OldTime<=T['20:00:00']) then WriteLn('It is now 8pm in the afternoon');
PAL.WaitForTime('23:59:59');

 

Please note that we do not want to be waiting inside an IF statement, so make sure to place the IF statement after your wait. (See below)  Running this script after the 10am will only result in the 8pm code being executed, provided the script was started before 8pm.  This totally solves the problem of scriptings being started late in the day causing unintended results.

Now, a few important concepts about waiting:

You can not wait inside

a) IF..THEN statements

b) CASE..OF statements

c) Custom functions & procedures

PAL will simply skip over the wait command. This is an unfortunate result of the implementation of the core language PAL was based on. This language was never meant to be execute line-by-line, but rather as a complete program. Thus we had to significantly modify this language to meet our needs. Unfortunately we were not able to work around this problem for the above mentioned statement blocks.

The good news is that there is ways to avoid this problem.

1. Do not wait inside functions & procedures. Rather repeat the source lines where needed.

2. In the case of IF..THEN and CASE..OF statements, use a WHILE..DO loop instead.

Example:


IF (A>B) THEN
begin
   PAL.WaitForPlayCount(1);
end;

 

Can be replaced with:


var Dummy : Boolean = True;
WHILE (A>B) AND (Dummy) DO
begin
PAL.WaitForPlayCount(1);
  Dummy := False;
end;

 

While obviously not the perfect solution, it gets the job done. Oh, another tip. You can use the Dummy variable if you have many replacements to do. You just have to remember to either set Dummy := True; before the while loop.

Speeding up execution

PAL was written to be a low priority process inside SAM Broadcaster so that the audio quality never gets affected. As a result PAL executes only line of code every second.

In program execution terms that are very slow - but for PAL this works perfect 99% of the time.

But there are moments where you need that instant execution. Luckily PAL makes this easy with these commands:


PAL.LockExecution;
{Do stuff}
PAL.UnlockExecution;

 

Example:


T := 0;
While (T<100) do
begin
   WriteLn(T);
   T := T + 1;
end;

 

This script will take at least 100 second to execute. By adding 2 lines, we can make the script execute in under 2 seconds.


var T : Integer;

PAL.LockExecution;
T := 0;
While (T<100) do
begin
   WriteLn(T);
   T := T + 1;
end;
PAL.UnlockExecution;

 

Be careful though - SAM Broadcaster will "freeze up" while a PAL script is in locked execution - thus do not stay locked for long periods of times. You can even check how long your code has been executing, and then take a short breather every now and then by either using a "WaitForXXX" command, or quickly unlocking and then locking execution again.

Objects

PAL is a fully OOP language. Although you will most likely not use much OOP yourself, PAL contains many objects which you need to be able to use.

(OOP = Object Orientated Programming)

Objects are almost like variables, except that objects can also perform actions via methods

Objects also store data via properties. Some properties are read only though, i.e. they can only be used to read values, not change the values.

Example:


ActivePlayer.FadeToNext; {Method}
ActivePlayer.Volume := 255; {Read/Write property}
WriteLn(ActivePlayer.Duration); {Read-only property}

var Obj : TPlayer;

Obj := ActivePlayer;
Obj.FadeToNext;

Objects support inheritance:
var Obj : TObject;
Obj := ActivePlayer;

if Obj is TPlayer then {Check if object is of the right type}
  TPlayer(Obj).FadeToNext; {TypeCast object to correct type and use it}

 

You can also create and destroy many of your own objects:


var List : TStringList;
List := TStringList.Create; {Create a stringlist object}
{Do stuff with object here}

 

List.Free; {Remember to free the object from memory once you are done with it}

Section 1minute overview:

Declaring variables:


const A = 'Hello world!';
const A = 123;

var V : Integer;
var V : String;
var V : Array[1..5] of Integer;
var V : Array of Integer;
var V : TMyClass;

var V : Integer = 1;
var V : String = 'Hello world!';

 

Math:


m := a + b;
m := a - b;
m := a * b;
m := a / b; {Float only}
m := a div b; {Integer only; Discards the remainder}
m := a mod b; {Integer only; Result is only the remainder}
m := -m;

 

Logic:


NOT a
a OR b
a AND b
a XOR b
a < b
a <= b
a > b
a >= b
a <> b
a = b

 

Casting:


Float(x)
DateTime(x)

 

Type checking & casting:


a is TMyClass
TMyClass(a)

 

Selection:


if {logic expression} then
{Do stuff}
else
{Do stuff};

 


case {Value} of
1       : {Do stuff}
2..4   : {Do stuff}
5,6,7 : {Do stuff}
else {Do stuff}
end;

 

Loops:


var C : Integer;
For C := A to B do
begin
   {Do stuff}
end;

 


while {Logic expression is True} do
begin
{Do stuff}
end;

 


repeat
{Do stuff}
until {Logic expression is True};

 

Waiting:


PAL.WaitForTime(TimeMask/DateTime);
PAL.WaitForPlayCount(SongCount);
PAL.WaitForQueue(NoOfItemsInQueue);

 

Speeding up execution:


PAL.LockExecution;
PAL.UnlockExecution;

 

Some warnings about PAL scripting

PAL scripting is a powerful programming language. With this power comes a lot of responsibility.

Incorrectly written PAL scripts can have many bad side effects, those include:

1. Cause SAM Broadcaster to lock up/freeze.

2. Cause exception errors in SAM Broadcaster, affecting the stability in SAM Broadcaster.

3. Although PAL does automatic garbage collection on script restart, it is possible to write scripts that consume all available memory if memory allocation is done in long-running loops.

4. PAL gives you direct access to your SQL database. It is possible to use SQL queries to completely erase or destroy the database and data integrity.

5. PAL gives you the power to work with files on your local hard disk and network. PAL can be used to delete or modify files.

Thus beware of example scripts you download from the internet. Review them first to make sure that they are written well and will not affect your broadcast negatively.  We also do not recommend developing PAL scripts on a production machine. It is best to have a mirror copy of SAM Broadcaster running on another machine, and then developing scripts on this machine.

Section 2 - The main SAM Broadcaster objects

Although SAM Broadcaster is a complex program, it can be described as 4 main components.

2.1. The playlist/media library of tracks.

2.2. Music selection logic & playlist rules.

2.3. The audio engine consisting of different audio channels represented by decks/players.

2.4. Finally, the encoders and statistic relays.

All other objects found in the PAL scripting language are basically used to support these main functions.  In this section we will discuss some of these main objects and use them in some real-world PAL scripts to demonstrate their functionality.

2.1 The media library/playlist

SAM Broadcaster stores EVERYTHING it played in the media library. Or more correct, SAM Broadcaster can only play a track if it exists in the media library. Without it SAM Broadcaster would not be able to do the advanced playlist management and selection logic rules needed for a professional radio station.

2.1.1 Importing or exporting files to/from the media library

Importing

You can add files to either a category or directly to the queue using various PAL commands.
A few examples:


CAT['MyCategory'].AddDir(‘c:\music\’, ipBottom);
CAT['MyCategory'].AddFile(‘c:\music\test.mp3’, ipBottom);
CAT['MyCategory'].AddList(‘c:\music\playlist.m3u’, ipBottom);
CAT['MyCategory'].AddURL(‘http://localhost:8000/test.mp3’, ipBottom);

 

The same commands can also be used for adding files to the queue


Queue.AddDir(‘c:\music\’, ipBottom);
Queue.AddFile(‘c:\music\test.mp3’, ipBottom);
Queue.AddList(‘c:\music\playlist.m3u’, ipBottom);
Queue.AddURL(‘http://localhost:8000/test.mp3’, ipBottom);

 

Exporting

Starting with SAM Broadcaster4, you can now easily store the contents of a category or the queue to file. This can be very useful when used with music scheduling software like M1 SE (http://www.spacialaudio.com/products/m1se)

Exporting a category can be done via the following PAL commands:


CAT['MyCategory'].SaveAsM3U('c:\list.m3u'); {Export as m3u playlist}
CAT['MyCategory'].SaveAsCSV('c:\list.csv'); {Export as CSV data file}

 

Exporting the queue can be done as follows:


Queue.SaveAsM3U('c:\list.m3u'); {Export as m3u playlist}
Queue.SaveAsCSV('c:\list.csv'); {Export as CSV data file}

 

Note: CSV files are spreadsheet files, also commonly known as comma-delimited files.

2.1.2 Using media library for custom rotation "clockwheels"

Serious broadcasters know the importance of properly tagging their music, i.e. making sure the artist, title, album and other fields are properly completed. Once that is done the music, jingles, promos and even advertisements are sorted into various categories.  Once all that hard work is done, the scheduling of music in a professional radio format becomes really easy.

In SAM Broadcaster music scheduling becomes the basic task of selecting a song from a certain category, and adding it to the queue.  When selecting a track from a category, SAM Broadcaster can use many different logics http://www.spacialaudio.com/products/sambroadcaster/help/sambroadcaster/Selection_Method_Logic.htm) and then apply certain selection rules (http://www.spacialaudio.com/products/sambroadcaster/help/sambroadcaster/Playlist_Rotation_Rules.htm) to get the best track that meets all these rules.

You can also specify if the track should go to the top or the bottom of the queue. We will show you later how to put this into good use.

So let’s jump in with a very simple script:


PAL.Loop := True;

Cat['Tracks'].QueueBottom(smLemmingLogic, EnforceRules);
Cat['Music (All)'].QueueBottom(smLRPA, EnforceRules);
Cat['Hot Hits'].QueueBottom(smLRP, EnforceRules);
Cat['Adz'].QueueBottom(smLRPA, NoRules);

PAL.WaitForPlayCount(4);

 

In the above script we first set the script to loop/restart.  Next we insert 4 tracks into the bottom of the queue. The first track will come from the "Tracks" category and we will use the random lemming logic to select the song, and then enforce our playlist rules on the selection of the song. We do similar, but slightly different things for the other 3 categories (see if you can spot and understand the difference!).

Finally, we wait for SAM Broadcaster to play 4 tracks, before the script ends and restarts/loops. (Why do we wait? What would happen if we did not wait?)

The final result of this script is that we play

One song from the category "Tracks"

One song from the category "Music (All)"

One song from the category "Hot Hitz"

One song from the category "Adz"

This format of programming will continue until the PAL script is stopped manually.  Inserting jingles, promos and station IDs into your format are also relatively simple.

Example:


PAL.Loop := True;

Cat['Tracks'].QueueBottom(smLemmingLogic, EnforceRules);
Cat['Music (All)'].QueueBottom(smLRPA, EnforceRules);
Cat['Hot Hits'].QueueBottom(smLRP, EnforceRules);
Cat['MyPromos'].QueueBottom(smLRP, NoRules);

PAL.WaitForPlayCount(4);

Cat['Tracks'].QueueBottom(smLemmingLogic, EnforceRules);
Cat['Music (All)'].QueueBottom(smLRPA, EnforceRules);
Cat['Hot Hits'].QueueBottom(smLRP, EnforceRules);
Cat['MyJingles'].QueueBottom(smLRP, NoRules);
PAL.WaitForPlayCount(4);

 

Notice how we had to repeat the song categories so we could place 1 Promo after a set and then 1 Jingle after another set - and have this repeat.

Also notice that I used the "NoRules" in the logic.

Let’s move on to a slightly more complex example. Here we will use two separate PAL scripts that run at the SAM Broadcaster time to produce our playlist logic.  PAL #1 will be responsible for scheduling our music, while PAL #2 will be responsible for adding jingles, promos and Adz to the system.

PAL 1


PAL.Loop := True;

Cat['Tracks'].QueueBottom(smLemmingLogic, EnforceRules);
Cat['Music (All)'].QueueBottom(smLRPA, EnforceRules);
Cat['Hot Hits'].QueueBottom(smLRP, EnforceRules);

PAL.WaitForQueue(0);

 

PAL 2


PAL.Loop := True;

PAL.WaitForPlayCount(5);
Cat['MyJingles'].QueueTop(smLRP, NoRules);

PAL.WaitForPlayCount(3);
Cat['MyPromos'].QueueTop(smLRP, NoRules);
Cat['MyAdz'].QueueTop(smLRP, NoRules);

 

A few things to pay attention to:

- We now use the WaitForQueue command in PAL 1 to know when to insert more songs into the queue.

- PAL 2 uses QueueTop now, because there might be songs in the queue - and we want to place this jingle/promo/ads content in the very next to-be-played spot at the top of the queue.

- Also, note that MyAdz will be placed above MyPromos in the queue. (In reverse order - make sure you understand why this happens)

2.1.3 Scheduling advertising in SAM Broadcaster (via StreamAdz)

Since SAM Broadcaster v4, SAM Broadcaster has direct integration with the powerful advertisement delivery platform called StreamAdz (http://www.spacialaudio.com/products/streamadz).

Instead of selecting a track from a category, you simply execute the StreamAdz PAL command to insert a specified duration of advertisements. SAM Broadcaster will then automatically select one or more advertisements to fill the specified advertisement block.

Example:


StreamAdz['Providers (All)'].QueueBottom(30); {Select an advertisement from any provider}
StreamAdz['Provider C'].QueueBottom(60); {Select an advertisement using ONLY provider C}

 

The following script demonstrated an easy advertisement scheduling script, inserting 2 minutes of advertising every 15 minutes.


PAL.Loop := True;
StreamAdz['Providers (All)'].QueueBottom(120); {Insert 2 minutes of advertising}
PAL.WaitForTime(‘+00:15:00’); {Wait 15 minutes}

 

2.2 Music selection logic & playlist rules.

Tracks stored in categories can be selected using various selection logic.  For example, smRandom will select a random track from the category while smLRP will select the song that has not played for the longest time. (Least Recently Played)

A song can only be selected if it complies with the Playlist logic rules. These rules aim to limit the amount of times a song, artist or album repeats within a period of time. For example it will be very bad to play the same song back-to-back! In the USA the DCMA laws even makes it illegal to repeat songs, artists or albums too frequently.

 Do-not-repeat rules do not however apply to jingles, promos, advertisements, etc. in the same way. That is why in general we use the NoRules flag when selecting these content types. However, you still do not want to repeat these too often. Playing the same jingle back-to-back or playing the same advertisement back-to-back is just as bad. The easiest way to avoid this is to use the smLRP rule. That way SAM Broadcaster will cycle through the content in a top-to-bottom fashion.

Even this fails though when inserting multiple tracks into the queue, because no repeat checking is applied - the same song will be added to the queue multiple times!

To solve this we need a special PAL script that modifies the playlist rules, insert the content, and then restores the old content. Now you are free again to use any selection logic method you wish!

Example:


{Declare a procedure that we will define later}
procedure NoRules(MyCat : String; Logic : Integer); forward;

PAL.Loop := True;
NoRules('MyAdz',smLRP);
NoRules('MyAdz',smRandom);
NoRules('MyAdz',smLRP);

PAL.WaitForPlayCount(8);

{========================================================}
procedure NoRules(MyCat : String; Logic : Integer);
var tArtist, tAlbum, tTitle  : Integer;

begin
{Save current rules so we can restore them later}
tArtist := PlaylistRules.MinArtistTime;
tTitle  := PlaylistRules.MinSongTime;
tAlbum  := PlaylistRules.MinAlbumTime;

{Set some very low repeat rules - 1 minute}
PlaylistRules.MinAlbumTime  := 1;
PlaylistRules.MinArtistTime := 1;
PlaylistRules.MinSongTime   := 1;

{Insert content to top of queue using our new rules}
CAT[MyCat].QueueTop(Logic,EnforceRules);

{Restore original rules}
PlaylistRules.MinArtistTime := tArtist;
PlaylistRules.MinSongTime   := tTitle;
PlaylistRules.MinAlbumTime  := tAlbum;
end;
{========================================================}

 

While SAM Broadcaster should be able to cater to all your needs, some people have very specific needs for their rotation. Luckily with the power of PAL, most have found ways to achieve their goals with some nifty scripting.

This PAL script applies 5 custom rules to tracks to decide what track to play next. The only downside to this script is that it takes a lot of CPU time to select a single song.

2.3. The audio engine consisting of different audio channels represented by decks/players.

PAL allows you to control various audio components in the audio engine.This can be very useful for making sure audio elements play exactly like you want them to.  Let’s go through the main components along with a few examples.

DeckA, DeckB, Aux1, Aux2, Aux3 and SoundFX are all player types (Class TPlayer)

This means that all of the same operations apply to all of these players. (Note that VoiceFX can currently not access via PAL since tracks should in general not be played over this player. This is meant exclusively for voice operation)

Reading currently playing song information

This script will read the song information of the track loaded into DeckA, and then print out the information.


var Song : TSongInfo;
Song := DeckA.GetSongInfo;

if Song = nil then
WriteLn('No song loaded into DeckA!')
else
begin
  WriteLn('Artist: '+Song['artist']);
WriteLn('Title: '+Song['title']);
  WriteLn('Album: '+Song['album']);
  WriteLn('Duration: '+Song['duration']);
end;

Song.Free;

 

Notice how we test if the Song = nil, because if that is the case trying to read information on the song object will cause memory access violations since the object does not exist!

Using utility functions

PAL comes with some very handy built-in functions you can use. These provide you with direct access to the correct player object. (Between DeckA & DeckB only!)

ActivePlayer

This returns the player object considered the "active" player. If only one deck is player a track, then this track will be the active player. If both decks are playing a track, then the deck with the longest duration remaining is considered the active deck.  If NEITHER DeckA or DeckB have any tracks playing, then this function returns NIL. It is very important that you check for the NIL value, unless you are certain that there will always be an active deck.

IdlePlayer

A Deck is considered Idle if no song is queued up in the deck, and the deck is not actively playing any tracks. If both decks are idle, DeckA is returned as the idle object. If both decks are queued up, or actively playing a track - then this function will return NIL!

QueuedPlayer

A player is considered in Queued mode if the Deck has a song object loaded into the Deck, but the Deck is not currently actively playing any tracks.  If both decks are either idle or active, then this function will return NIL! Otherwise it will return the first Queued deck.

Overall the ActivePlayer function is used much more than any of the others. Most of our examples will deal with this function.

For example check if SAM Broadcaster is active:


if (ActivePlayer = nil) then
WriteLn('WARNING!!! Nothing is playing??');

 

This script will wait for the top-of-the-hour and then insert a news item. It will then immediately fade to this news item.


PAL.Loop := True;
PAL.WaitForTime('XX:00:00');
Queue.AddFile('c:\news\news.mp3',ipTop);
ActivePlayer.FadeToNext;

 

Here is a bit more of a tricky script. This one will insert some liners over the music, but it will only do so if:

a) The music has an intro of more than 5 seconds

b) The music is a normal song (i.e. not a promo, jingle, advertisement, news)


{ About:
This script will play a liner in Aux1 as soon as a new track starts
  The liner will only be played if
  a) The song has an intro of specified minimum duration
  b) The song is of type S, i.e. a normal song.

  Then the script will wait the specified amount of time before it tries to play another liner.

This script can help brand your station and make it sound like a true commercial terrestrial station.

  Usage:
  a) Make sure you use the song information editor to specify intro times for your tracks!
  b) Make sure the AGC settings on Aux1 is to your liking. Also set the volume a bit louder
     on Aux1 so you can clearly hear the liner above the active Deck audio.
  c) Edit the configuration details below.

  Make sure to change the category to the one you use to store your liners.
}

{ CONFIGURATION }
{==================================================}
const MIN_INTRO = 5*1000; //5 seconds
const MIN_WAIT  = '+00:15:00'; //Wait 15 minutes between liners
const LINERS_CATEGORY = 'Liners';

{ IMPLEMENTATION }
{--------------------------------------------------}
function ExtractIntro(Song : TSongInfo):Integer; forward;

var Song, Liner : TSongInfo;
var Waiting : Boolean = True;
var Intro : Integer = 0;

Aux1.Eject;

{Step1: Queue up the deck, ready for play}
Liner := CAT[LINERS_CATEGORY].ChooseSong(smLRP,NoRules);
if (Liner=nil) then
WriteLn('No valid liner found')
else if (not Aux1.QueueSong(Liner)) then
WriteLn('Failed to queue song: '+Liner['filename']);

{Wait for a valid song with intro}
while Waiting do
begin
  {Step2: Wait for the song to change}
  PAL.WaitForPlayCount(1);

  {Step3: Grab current song information}
  Song := ActivePlayer.GetSongInfo;

  if (Song=nil) then
   WriteLn('The active player contained no song info??')
  else
   begin
    {Extract the intro time - this is a bit tricky}
    Intro := ExtractIntro(Song);
    {Start playing the liner if the current song matches our rules}
    if(Song['songtype']='S') and (Intro>=MIN_INTRO) then
     begin
      Aux1.Play;
      Waiting := False;
     end;
    Song.Free; Song := nil;
   end;
end;

{Wait 5 minutes before we do this all again}
PAL.WaitForTime(MIN_WAIT);
PAL.Loop := True;

{................................................}
function ExtractIntro(Song : TSongInfo):Integer;
var P : Integer;
XFade : String;

begin
Result := -1;
XFade := Trim(Song['xfade']);
WriteLn('Decoding XFade string');
WriteLn('XFade: '+XFade);
if XFade = '' then
  Result := -1
else
  begin
P := Pos('&i=',XFade);
   if (P > 0) then
begin
     Delete(XFade,1,P+2);
     P := Pos('&',XFade);
     if (P>0) then
      Delete(XFade,P,Length(XFade));
     Result := StrToIntDef(XFade,-1);
WriteLn('Intro time detected: '+XFade);
    end;
  end;
end;
{--------------------------------------------------}

 

Other methods and functions

Here follows a quick discussion on other methods and properties available in the TPlayer object, and how they may be used.

Properties:

The CurTime property can be monitored to see when a song is approaching its end (when compared to the Duration property) and then some action can be taken right before the next song is started or queued.

Example:


Var Done : Boolean = False;

while not done do
begin
  if((ActivePlayer.Duration>0) AND ((ActivePlayer.Duration-ActivePlayer.CurTime)>10000)) then
   begin
      WriteLn('This song will end in 10 seconds!');
      Done := True;
   end;
end;

 

Note: Due to the gap killer, it may be less than 10 seconds. You can use the actual value of the calculation to get the correct value of the time remaining.

You can read and write the Volume property of a Deck. This can be used to player advertisements, stationIDs, etc. louder than other tracks.  We recommend you use the song information editor to set the Gain for each track of these audio content types, but this script might be an alternative solution.


var Done : Boolean = False;
var Song : TSongInfo;

While not Done do
begin
if QueuedPlayer <> nil then
  begin
    Done := True;
    Song := QueuedPlayer.GetSongInfo;
    if (Song<>nil) then
     case song['songtype'] of
      'S' : QueuedPlayer.Volume := 255;
      'A','J','P' : QueuedPlayer.Volume := 350;
       else QueuedPlayer.Volume := 255;
     end; //case
end;
end;

PAL.Loop := True;
PAL.WaitForPlayCount(1);

 

Once again, this is not the recommended method - but it does serve as a good example of how to use the volume option in PAL.  Note:  255 is full volume, while 350 is volume with extra gain applied.

Using the above as reference, you should now be able to also create a script that can adjust the tempo or pitch of the deck according to what song is currently queued in it.  This can be used for a script that does automatic beat matching for example...

Please refer to the PAL manual about the various other properties and methods we did not discuss in this chapter.

2.4. Encoders and statistic relays

PAL also gives you access to the encoders & statistic relays.

This gives you the power to start/stop the encoders - as well as insert scripting data into the stream.

We will cover some of these techniques in the following sections.

2.4.1 Statistic relays

The Relays class basically allows you the power to do 5 main things:

- Read the status of all the relays

- Read the current listener count

- Read the peak listener count

- Read the maximum available listener slots

- Force an update of the statistic relays.

Example:


{Print out relays information - all combined}

PAL.LockExecution;

WriteLn('Relays overview ');
WriteStr('--Active: ');

if Relays.AtLeastOneActive then WriteLn('Online') else WriteLn('Offline');
WriteStr('--Viewers: '); WriteLn(Relays.Viewers);
WriteStr('--Peak: '); WriteLn(Relays.Viewers_High);
    WriteStr('--Max: '); WriteLn(Relays.Viewers_Max);
    WriteLn('');

PAL.UnlockExecution;

 


{Print out relays information - each relay on its own}

var I : Integer;

PAL.LockExecution;

for I := 0 to Relays.Count-1 do
begin
    WriteStr('Relay number '); WriteLn(I);
   WriteStr('--Active: '); WriteLn(Relays[I].Active);
    WriteStr('--Status: '); WriteLn(Relays[I].Status);
    WriteStr('--Viewers: '); WriteLn(Relays[I].Viewers);
    WriteStr('--Peak: '); WriteLn(Relays[I].Viewers_High);
    WriteStr('--Bitrate (kbps): '); WriteLn(Relays[I].Bitrate);
    WriteStr('--Format: '); WriteLn(Relays[I].Format);
    WriteLn('');
end;

PAL.UnlockExecution;

 

2.4.2 Encoders

Starting/Stopping encoders

This is a simple script to start all encoders at 7am in the morning, and then stop them again at 8pm:


PAL.Loop := True;

PAL.WaitForTime('07:00:00');
Encoders.StartAll;

PAL.WaitForTime('20:00:00');
Encoders.StopAll;

 

You can also start/stop a single encoder by using the encoder array index. (Remember, we count from zero)


PAL.Loop := True;

PAL.WaitForTime('07:00:00');
Encoders[0].Start;

PAL.WaitForTime('20:00:00');
Encoders[0].Stop;

 

Archiving

SAM Broadcaster has the option to specify that an encoder does absolutely no streaming, but will be used to archive the content only.  This allows you to create a second encoder whose sole duty is to archive the stream.

Here is an example script which is meant for two encoders, where encoder0 is the actual streaming encoder, and encoder1 is the archiving encoder.  This script will cause the archive to be split into multiple sections each hour, but restarting the encoders on the hour. Note that this will not affect the streaming encoder at all since it never gets stopped!


PAL.Loop := True;

PAL.WaitForTime('XX:00:00');
Encoders[1].Stop;
Encoders[1].Start;

 

Switching DJs

A lot of stations have more than one DJ operating the station, most of the time these DJ's are spread across the world. They employ a nifty trick to switch between DJ sessions. If you new DJ wishes to start their session, they log into the SHOUTcast admin panel and kick the current DJ's encoder, and then quickly connect with their own encoder.

The following PAL script can completely automate this process:


{ About:
  This script will disconnect any source connected to a SHOUTcast server
and then connects this SAM Broadcaster as the new source.

  Usage:
  a) Create a single MP3 encoder to connect to the SHOUTcast server.
  b) Supply your SHOUTcast server details in the configuration section below
  c) Use the Event Scheduler to start this PAL script at the correct time.
}

{ CONFIGURATION }
{==================================================}
const shoutcast_password = 'changeme';
const shoutcast_host     = 'localhost';
const shoutcast_port     = '8000';
{==================================================}

{ IMPLEMENTATION }
{--------------------------------------------------}
{ Build URL used to send command to SHOUTcast server }
var URL : String;
URL := 'http://admin:'+shoutcast_password+'@'+shoutcast_host+':'+shoutcast_port+'/admin.cgi?mode=kicksrc';

{ Kick source from SHOUTcast server }
WebToFile('c:\dummy.txt',URL);

{ Now start & connect all encoders }
{ NOTE: This assumes you only have one encoder }
Encoders.StartAll;

{TIP: Use this to start a specific encoder:
Encoders[0].Start;
}
{--------------------------------------------------}

 

Changing song titles

The encoder class also allows you to update the current song information or title data being sent to the streaming server. See TitleStreamBanners.pal for an example.

Note: This can also be done for each individual encoder, in case it does not make logical sense to change the title data on all your active encoders. For example if you have an archiving encoder, you most likely do not want to update the titles in this.

Embedded scripts

While SAM Broadcaster is one of the few encoders (in fact the only one we are aware of) that supports embedded ID3v2 tags inside MP3 streams, due to lack of player support we will only discuss Windows media scripts here.

Windows Media allows for embedded scripts in the audio data. These scripts can be used for displaying current song data, captioning information or synchronizing external pictures and text of the current audio. We use this in our StreamAdz system to synchronize text and banner advertising with audio advertising.

The following example PAL script is for display the current local time in the captioning area of the Windows Media player every minute.

Please note: For you to be able to see the captioning data, you must enable the captioning view inside your player.

Menu->Play->Captions and Subtitles->On if available


PAL.Loop := True;
PAL.WaitForTime('XX:XX:00');
Encoders.WriteScript('CAPTION','The local time is now '+TimeToStr(Now));

Was this article helpful?
5 out of 5 found this helpful
Have more questions? Submit a request

Comments