Scripting-Hilfe: Zählvariablen

So umfangreich die Funktionen in mAirList auch sind, so fehlt doch immer wieder mal das eine oder andere Feature für den gewünschten Zweck. Macht aber nichts, denn genau zu diesem Zweck gibt es die Scripting-Engine, mit welcher die Möglichkeiten in mAirList noch wesentlich umfangreicher werden als ohnehin schon. Also:

Zählvariablen

Habt Ihr irgendwo im Skript mehrere gleichlautende Anweisungen, die sich auf durchnumerierte Elemente beziehen, die also nacheinander abgearbeitet werden sollen? Ich gebe mal ein Beispiel, fünf Buttons, die alle scharfgeschaltet werden sollen:

begin
  ExecuteCommand('BUTTON.1 ENABLE');
  ExecuteCommand('BUTTON.2 ENABLE');
  ExecuteCommand('BUTTON.3 ENABLE');
  ExecuteCommand('BUTTON.4 ENABLE');
  ExecuteCommand('BUTTON.5 ENABLE');
end;

Das sieht irgendwie nach zuviel, nach redundant aus. Alleine das eintippen ist ja schon doof! In der Tat, und deshalb gibt es dafür auch einen Trick, es einfacher zu gestalten: Die FOR … DO-Anweisung. Sie klingt schon nach Arbeit (also für sie, nicht für uns!), duch das DO – „tu was!“ Mit ihrer Hilfe lassen sich solche Aufgaben elegant vereinfachen:

var
  i: integer;

begin
  for i := 1 to 5 do
    ExecuteCommand('BUTTON.' + IntToStr(i) + ' ENABLE');
end;

Was ist hier passiert? Die Zählvariable i wird von 1 bis 5 durchgezählt und danach jeweils die Anweisung mit dem entsprechenden Index ausgeführt.

Damit mAirList das Ergebnis auch versteht, müssen wir noch zweimal in die Trickkiste greifen. Erstens: Die Befehle, die nichts weiter als sogenannte Zeichenketten, also eine Aneinanderreihung von Buchstaben oder Ziffern sind, lassen sich auch verknüpfen – im einfachsten Falle addieren, also aneinanderhängen. Die Zuweisung

s := 'BIER';
s := 'FREI' + s;

ergibt tatsächlich

'FREIBIER'

Und, zweitens, die Zählvariable i läßt sich nicht ohne weiteres in die Zeichenkette einbauen. Die Integer-Zahl 5 ist etwas völlig anderes als die Zeichenkette '5', obwohl sie eigentlich gleich aussieht.* Zur Abhilfe gibt es aber die Funktion IntToStr, Integer to String soll das wohl heißen, und die macht aus einer solchen Zahl eine Zeichenkette. Und damit lassen sich dann solche Anweisungen wie oben

ExecuteCommand('BUTTON.' + IntToStr(i) + ' ENABLE');

zusammenbauen, so daß am Ende wieder (lassen wir es beim Beispiel i := 5)

ExecuteCommand('BUTTON.5 ENABLE');

herauskommt. Wichtig ist übrigens das führende Leerzeichen bei ' ENABLE', damit hinten die richtige Zeichenfolge herauskommt.

Soll mehr als nur eine Zeile bearbeitet werden, verpackt Ihr das ganze in begin und end;:

begin
  for i := 1 to 5 do
    begin
      ExecuteCommand('BUTTON.' + IntToStr(i) + ' ENABLE');
      ExecuteCommand(<irgendein Befehl>);
      ExecuteCommand(<noch ein Befehl>);
      sleep(35);
    end;
end;

Dann wird alles zwischen begin und end; so oft wiederholt, wie die Schleife dauert.

Das ganze kann man sogar für nützliche Dinge einsetzen: Ich zitiere mal das Skript des Forenkollegen @shorty.xs, der sich das ausgedacht hatte, um auf ein bestimmtes Ereignis hin seine Player um einen bestimmten Betrag herunterzublenden und auf ein anderes Ereignis wieder herauf:

(Da Malte seinen Code ohnehin öffentlich zur Schau stellt, bin ich mal so frei, ihn hier wiederzugeben.)

{
:: This is a mAirlist Script file, that lowers the volume of the first 2 Players of a Playlist. The Default MIC-ON function lowers all Player inputs and does not allow any selection.
:: This file needs to be registered as a "Background Script" in mAirlist COnfigration.
:: @package     
:: @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
:: @author      Malte Schroeder <post@malte-schroeder.de>
:: @copyright   Copyright (c) 2011-2019 Malte Schroeder (http://www.malte-schroeder.de)

}

procedure OnEncoderInputToggle(Input: TEncoderInput; NewState: boolean);  
begin
  if (Input = eiMic) and (NewState = false) then begin
    ExecuteCommand('PLAYER 1-1 VOLUME -12');
    ExecuteCommand('PLAYER 1-2 VOLUME -12');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -11');
    ExecuteCommand('PLAYER 1-2 VOLUME -11');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -10');
    ExecuteCommand('PLAYER 1-2 VOLUME -10');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -9');
    ExecuteCommand('PLAYER 1-2 VOLUME -9');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -8');
    ExecuteCommand('PLAYER 1-2 VOLUME -8');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -7');
    ExecuteCommand('PLAYER 1-2 VOLUME -7');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -6');
    ExecuteCommand('PLAYER 1-2 VOLUME -6');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -5');
    ExecuteCommand('PLAYER 1-2 VOLUME -5');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -4');
    ExecuteCommand('PLAYER 1-2 VOLUME -4');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -3');
    ExecuteCommand('PLAYER 1-2 VOLUME -3');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -2');
    ExecuteCommand('PLAYER 1-2 VOLUME -2');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -1');
    ExecuteCommand('PLAYER 1-2 VOLUME -1');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME 0');
    ExecuteCommand('PLAYER 1-2 VOLUME 0');
    
 end
 else if (Input = eiMic) and (NewState = true) then begin
    ExecuteCommand('PLAYER 1-1 VOLUME 0');
    ExecuteCommand('PLAYER 1-2 VOLUME 0');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -1');
    ExecuteCommand('PLAYER 1-2 VOLUME -1');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -2');
    ExecuteCommand('PLAYER 1-2 VOLUME -2');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -3');
    ExecuteCommand('PLAYER 1-2 VOLUME -3');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -4');
    ExecuteCommand('PLAYER 1-2 VOLUME -4');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -5');
    ExecuteCommand('PLAYER 1-2 VOLUME -5');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -6');
    ExecuteCommand('PLAYER 1-2 VOLUME -6');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -7');
    ExecuteCommand('PLAYER 1-2 VOLUME -7');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -8');
    ExecuteCommand('PLAYER 1-2 VOLUME -8');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -9');
    ExecuteCommand('PLAYER 1-2 VOLUME -9');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -10');
    ExecuteCommand('PLAYER 1-2 VOLUME -10');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -11');
    ExecuteCommand('PLAYER 1-2 VOLUME -11');
    Sleep(100);
    ExecuteCommand('PLAYER 1-1 VOLUME -12');
    ExecuteCommand('PLAYER 1-2 VOLUME -12');   
 end
end;

Das sind fast hundert Zeilen Code, die nichts anderes machen als die Player nacheinander immer wieder um 1 dB ab- oder aufzublenden. Mit der FOR … DO-Schleife sieht es jedoch so aus:

{
:: This is a mAirlist Script file, that lowers the volume of the first 2 Players of a Playlist. The Default MIC-ON function lowers all Player inputs and does not allow any selection.
:: This file needs to be registered as a "Background Script" in mAirlist Configration.
:: @package     
:: @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
:: @author      Malte Schroeder <post@malte-schroeder.de> & Tondose 
:: @copyright   Copyright (c) 2011-2019 Malte Schroeder (http://www.malte-schroeder.de) & Tondose (https://community.mairlist.com/u/Tondose)

}

var
  i, target, delay : integer;

procedure OnEncoderInputToggle(Input: TEncoderInput; NewState: boolean);  
begin
target := -12; // set your target duck range here
delay := 100; // set the delay betwee steps here to smoothen the fade

  if (Input = eiMic) and (NewState = false) then begin
    for i := (target) to 0 do
      begin
        ExecuteCommand('PLAYER 1-1 VOLUME ' + IntToStr(i));
        ExecuteCommand('PLAYER 1-2 VOLUME ' + IntToStr(i));
        Sleep(delay);
      end;
  end
  else if (Input = eiMic) and (NewState = true) then begin
    for i := 0 downto (target) do
      begin
        ExecuteCommand('PLAYER 1-1 VOLUME ' + IntToStr(i));
        ExecuteCommand('PLAYER 1-2 VOLUME ' + IntToStr(i));
        Sleep(delay);
      end;
  end;
end;

Zack, sechzig Zeilen weniger! (Sieht man von den Kopfzeilen ab, ist das Ergebnis noch sparsamer.) Ist das nichts?!

Eine Feinheit sei noch erwähnt: Beim Teil, in dem abgeblendet wird, müssen die dB ja runter-, also rückwärts gezählt werden. Auch das geht: dann schreibt ihr in die Anweisung FOR … DOWNTO, und schon läuft die Sache achteraus.

Gezählte Grüße

TSD


* Auch „Ketten“ aus einem einzigen Glied heißen bei Delphi „Zeichenkette“.


Edit: Wie immer ein Haufen Tippfehler.

Ergänzung:

Das bisher gesagte ist alles ganz prima, wenn man die Objekte der Reihe nach abfrühstücken möchte. Will man zum Beispiel von fünf Buttons (BUTTON.1 bis BUTTON.5) einen nur einzigen (z. B. die Nummer 4) einschalten und alle anderen sollen ausgehen, dann könnte man das so lösen:

var
  i: integer

begin
  for i := 1 to 5 do
    begin
      ExecuteCommand('BUTTON.' + IntToStr(i) + ' OFF'); // Hier werden zunächst alle abgeschaltet.
    end;
  ExecuteCommand('BUTTON.4 ON');                        // Und hier der vierte eingeschaltet.
end.

Was aber, wenn mehrere bestimmte Buttons zugleich eingeschaltet werden sollen? Man könnte das mit einem Haufen if/then-Abfragen erledigen, aber dann wird der Code wieder sperrig. Für diesen Fall gibt es die case-Anweisung dieses Formats:

var
  k: integer;

begin

  case k of
   1:             // wenn k=1, dann …
    begin
                  // tu was
    end;
   2,3,4:         //wenn k=2 oder 3 oder 4, dann …
    begin
                  // tu auch was
    end;
   5..8:          // wenn k=5 oder 6 oder 7 oder 8, dann …
    begin
                  // tu wieder was
    end;
  else            // ansonsten … 
    begin
                  // tu was anderes
    end;
  end;            // und zum Abschluß ein end;

end;

Das else ist optional und, im Gegensatz zum else beim if/then, es steht davor ein Semikolon.

Wie können wir das jetzt nutzbringend verwerten?

In diesem Thread möchte @ELBE-Tom eine Reihe von Streams, aber immer anderer, aufschalten und dazu je ein Lämpchen (also einen Button) leuchten lassen. Eine Lösung dafür sieht so aus:

begin
  Encoder.GetConnections.GetItem(0).SetEnabled(false);
  ExecuteCommand('BUTTON.0 OFF');
  Encoder.GetConnections.GetItem(1).SetEnabled(true);
  ExecuteCommand('BUTTON.1 ON');
  Encoder.GetConnections.GetItem(2).SetEnabled(true);
  ExecuteCommand('BUTTON.2 OFF');
  Encoder.GetConnections.GetItem(3).SetEnabled(false);
  ExecuteCommand('BUTTON.3 OFF');
  Encoder.GetConnections.GetItem(4).SetEnabled(false);
  ExecuteCommand('BUTTON.4 OFF');
  Encoder.GetConnections.GetItem(5).SetEnabled(true);
  ExecuteCommand('BUTTON.5 ON');
  Encoder.GetConnections.GetItem(6).SetEnabled(false);
  ExecuteCommand('BUTTON.6 OFF');
  Encoder.GetConnections.GetItem(7).SetEnabled(false);
  ExecuteCommand('BUTTON.7 OFF');
  Encoder.GetConnections.GetItem(8).SetEnabled(true);
  ExecuteCommand('BUTTON.8 ON');
  Encoder.GetConnections.GetItem(9).SetEnabled(true);
  ExecuteCommand('BUTTON.9 ON');
  Encoder.GetConnections.GetItem(10).SetEnabled(true);
  ExecuteCommand('BUTTON.10 ON');
  Encoder.GetConnections.GetItem(11).SetEnabled(true);
  ExecuteCommand('BUTTON.11 ON');
  Encoder.GetConnections.GetItem(12).SetEnabled(true);
  ExecuteCommand('BUTTON.12 ON');
  Encoder.GetConnections.GetItem(13).SetEnabled(false);
  ExecuteCommand('BUTTON.13 OFF');
  Encoder.GetConnections.GetItem(14).SetEnabled(true);
  ExecuteCommand('BUTTON.14 ON');
  Encoder.GetConnections.GetItem(15).SetEnabled(false);
  ExecuteCommand('BUTTON.15 OFF');
end.

Das ist aber wieder so eine Lösung, die ich weiter oben als „doof“ bezeichnet habe, wegen der vielen Tipparbeit. Man bedenke, daß für jede gewünschte Kombination von aufzuschaltenden Streams ein solches Skript erstellt werden muß. Es ist auch ziemlich unübersichtlich. Eleganter geht es hiermit:

{
Zwischen die geschweiften Klammern kann der Spickzettel hin, z.B.:

1: SWR, 2: HR, 3: RIAS.
4: NDR, 5: RB; 6: ORF
usw.
}

const
  iMax = 15;          // Hier die maximale Senderanzahl minus 1 einsetzen.

var
  i: integer;
  Stream: array[0 .. iMax] of boolean;
  Button: array[0 .. iMax] of string;

begin
 for i := 0 to iMax do
  case i of
  1, 5, 8..12, 14 :   // Hier aufzuschaltende Sender durch Kommata getrennt eintragen.
    begin
      Stream[i] := true;
      Button[i] := ' ON';
    end;
  else
    begin
      Stream[i] := false;
      Button[i] := ' OFF';
    end;
  end;

for i := 0 to iMax do
  begin 
    Encoder.GetConnections.GetItem(i).SetEnabled(Stream[i]);
    ExecuteCommand('BUTTON.' + IntToStr(i) + Button[i]);
  end;

end.

Was passiert hier also? Ich versuche es mal aufzudröseln, ich wollte das Skript nicht mit zuvielen Kommentarzeilen verstopfen:

  • Zunächst: Kommentare kann man auch zwischen geschweiften Klammern {} schreiben, alles dazwischen wird ignoriert.

  • const iMax = 15;
    

    Hier wird eine Konstante definiert, d. h. eine Größe, die sich nie ändert. Hier iMax genannt, die maximale Anzahl der Sender (minus eins, da nullbasiert).

  • var
      i: integer;
      Stream: array[0 .. iMax] of boolean;
      Button: array[0 .. iMax] of string;
    

    Mit dem array wird praktisch ein Schränkchen mit einer Anzahl (nämlich iMax + 1) Schubladen aufgestellt, die zunächst freilich noch leer sind. Später kommen da Variablen, also Werte, rein. Die Schubladen sind durchnumeriert, und in jedes Schränkchen passen jeweils nur Werte eines bestimmten Typs. Hier einmal boolean, d. h. wahr/falsch-Werte und einmal string, das sind Buchstaben.

  • for i := 0 to iMax do
    

    Es wird nun jede Schublade von oben nach unten durchgezählt.

  • case i of
    1, 5, 8..12, 14 :
    

    Hier kommt jetzt dieses case ins Spiel: In diesem Beispiel werden wie oben die Streams 1, 5, 8, 9, 10, 11, 12 und 14 angesprochen (die sollen aufgeschaltet werden), die Streams 0, 2, 3, 4, 6, 7, 13 und 15 gehen leer aus. Sollen andere Streams aufgeschaltet werden, braucht man lediglich diese Zahlen zu verändern.

    Noch ein Trick: Fortlaufende Zahlen können zusammengefaßt werden, hier 8..12, das bedeutet also die Werte 8, 9, 10, 11 und 12.

  • Stream[i] := true;
    Button[i] := ' ON';
    

    Jetzt werden die Schubladen gefüllt, und zwar im Falle („case“) der oben genannten Streams 1, 5, 8, usw. Jeder Stream hat zwei Schubladen, eine im Schränkchen Stream, und eine im Schränkchen Button. Die jeweiligen Stream-Schubladen bekommen den Wert true eingelegt, in die Button-Schubladen kommt eine Zeichenkette, nämlich ' ON' (mit Apostrophen und Leerzeichen).

  • else
      begin
        Stream[i] := false;
        Button[i] := ' OFF';
      end;
    

    Alle anderen Schubladen werden mit false bzw. ' OFF' belegt.

  • Encoder.GetConnections.GetItem(i).SetEnabled(Stream[i]);
    

    Hier werden die Schubladen aufgemacht und die einzelnen Encoder (i) nacheinander mit dem Wert aus ihrem Schubfach (Stream[i]) versehen, also aufgeschaltet, wenn Stream[i] = true und umgekehrt.

  • ExecuteCommand('BUTTON.' + IntToStr(i) + Button[i]);
    

    Und hier wird einmal der Befehl BUTTON. mit der passenden Nummer versehen (IntToStr(i)) und dann der gewünschte Schaltzustand aus dem entsprechenden Schubkästchen geholt und angehängt.

Das sieht zunächst komplizierter aus als als das obige Skript, ist es tatsächlich aber, zumindest von der Anwendung her, nicht. Vielleicht kann der eine oder andere sich ja diese Beispiele zunutze machen und etwas für sich abzweigen.

@Torben: Sollte ich hier Unsinn posten, bitte schnell eingreifen!

Bestimmte Grüße

TSD

Unsinn nicht, aber den Teil mit dem Button[i] kannst du dir durchaus sparen, wenn du stattdessen eine if/then/else-Abfrage einbaust.

Ich würde mir für den Zweck eine hübsche kleine Hilfsprozedur schreiben, die sowohl den Verbindungsstatus setzt als auch den Button aktualisiert:

procedure SetConnectionEnabled(Index: integer; State: boolean);
begin
  Encoder.GetConnections.GetItem(Index).SetEnabled(State);
  if State then
    ExecuteCommand('BUTTON.' + IntToStr(Index) + ' ON')
  else
    ExecuteCommand('BUTTON.' + IntToStr(Index) + ' OFF');
end;

Genial!

Damit sähe das Skript dann so aus:

{
Zwischen die geschweiften Klammern kann der Spickzettel hin, z.B.:

1: SWR, 2: HR, 3: RIAS.
4: NDR, 5: RB; 6: ORF
usw.
}

const
  iMax = 15;          // Hier die maximale Senderanzahl minus 1 einsetzen.

var
  i: integer;
  Stream: array[0 .. iMax] of boolean;

procedure SetConnectionEnabled(Index: integer; State: boolean);
begin
  Encoder.GetConnections.GetItem(Index).SetEnabled(State);
  if State then
    ExecuteCommand('BUTTON.' + IntToStr(Index) + ' ON')
  else
    ExecuteCommand('BUTTON.' + IntToStr(Index) + ' OFF');
end;

begin
 for i := 0 to iMax do
  case i of
  1, 5, 8..12, 14 :   // Hier aufzuschaltende Sender durch Kommata getrennt eintragen.
      Stream[i] := true;
  else
      Stream[i] := false;
  end;

for i := 0 to iMax do
  SetConnectionEnabled(i , Stream[i]);

end.

(Bisher ungetestet. Wer mag es mal ausprobieren? @ELBE-Tom?)

Weiter verkürzte Grüße

TSD


Edit: Komma statt Semikolon im Prozuduraufruf.