//OSCUtils //////description //Utility library to encode/decode osc-packets //inspired by original OSC reference implementation (OSC-Kit) //and OSC.Net library as shipped with the TUIO-CSharp sample //from http://reactable.iua.upf.edu/?software //////licence //GNU Lesser General Public License (LGPL) //english: http://www.gnu.org/licenses/lgpl.html //german: http://www.gnu.de/lgpl-ger.html //////language/ide //delphi //////initial author //joreg -> joreg@vvvv.org //additions for FreePascal //simon -> simonmoscrop@googlemail.com //////instructions ////for use with FreePascal //define: FPC ////encoding a single message: //first create a message: msg := TOSCMessage.Create(address) //then call msg.AddAsString(typetag, value) to add any number of arguments //with msg.ToOSCString you get its osc-string representation ////encoding a bundle: //first create a bundle: bundle := TOSCBundle.Create //then add any number of packets (i.e. message, bundle) via bundle.Add(packet) //with bundle.ToOSCString you get its osc-string representation ////decoding a string //use TOSCPacket.Unpack(PByte(osc-string), Length(osc-string)) to create //TOSCPackets of your osc-strings (those can be either bundles or single //messages. if you want to decode several packets at once you can create //a container bundle first and add the packets you create like this. //then use msg := FPacket.MatchAddress(address) to find a message with the //according address in your packet-structure. //before you now can access the arguments and typetags of a message you have //to call msg.Decode //voila. unit OSCUtils; interface uses Classes, Contnrs; type TOSCPacket = class; TOSCMessage = class; TOSCPacket = class (TObject) private protected FBytes: PByte; function MatchBrackets(pMessage, pAddress: PChar): Boolean; function MatchList(pMessage, pAddress: PChar): Boolean; function MatchPattern(pMessage, pAddress: PChar): Boolean; public constructor Create(Bytes: PByte); function MatchAddress(Address: String): TOSCMessage; virtual; abstract; function ToOSCString: string; virtual; abstract; procedure Unmatch; virtual; abstract; class function Unpack(Bytes: PByte; Count: Integer): TOSCPacket; overload; class function Unpack(Bytes: PByte; Offset, Count: Integer; TimeTag: Extended = 0): TOSCPacket; overload; virtual; end; TOSCMessage = class(TOSCPacket) private FAddress: string; FArguments: TStringList; FIsDecoded: Boolean; FMatched: Boolean; FTimeTag: Extended; FTypeTagOffset: Integer; FTypeTags: string; function GetArgument(Index: Integer): string; function GetArgumentCount: Integer; function GetTypeTag(Index: Integer): string; public constructor Create(Address: string); overload; constructor Create(Bytes: PByte); overload; destructor Destroy; override; function AddAsString(TypeTag: Char; Value: String): HResult; procedure AddFloat(Value: Single); procedure AddInteger(Value: Integer); procedure AddString(Value: String); procedure Decode; function MatchAddress(Address: String): TOSCMessage; override; function ToOSCString: string; override; procedure Unmatch; override; class function Unpack(Bytes: PByte; PacketOffset, Count: Integer; TimeTag: Extended = 0): TOSCPacket; overload; override; property Address: string read FAddress write FAddress; property Argument[Index: Integer]: String read GetArgument; property ArgumentCount: Integer read GetArgumentCount; property IsDecoded: Boolean read FIsDecoded write FIsDecoded; property Matched: Boolean read FMatched write FMatched; property TimeTag: Extended read FTimeTag write FTimeTag; property TypeTag[Index: Integer]: String read GetTypeTag; property TypeTagOffset: Integer read FTypeTagOffset write FTypeTagOffset; end; TOSCBundle = class(TOSCPacket) private FPackets: TObjectList; public constructor Create(Bytes: PByte); destructor Destroy; override; procedure Add(const Packet: TOSCPacket); function MatchAddress(Address: String): TOSCMessage; override; function ToOSCString: string; override; procedure Unmatch; override; class function Unpack(Bytes: PByte; PacketOffset, Count: Integer; TimeTag: Extended = 0): TOSCPacket; overload; override; end; function MakeOSCFloat(value: Single): String; function MakeOSCInt(value: Integer): String; function MakeOSCString(value: String): String; function UnpackFloat(Bytes: PByte; var Offset: Integer): Single; function UnpackInt(Bytes: PByte; var Offset: Integer): Integer; function UnpackString(Bytes: PByte; var Offset: Integer): string; const OSC_OK = 0; OSC_UNRECOGNIZED_TYPETAG = 1; OSC_CONVERT_ERROR = 2; implementation uses SysUtils, Math {$IFNDEF FPC}, WinSock {$ENDIF}; function MakeOSCFloat(value: Single): String; var tmp: Byte; intg, i: Integer; begin result := ''; intg := Integer(Pointer(value)); {$IFDEF FPC} intg := BEtoN(intg); {$ELSE} intg := htonl(intg); {$ENDIF} for i := 0 to 3 do begin tmp := intg and $ff; result := result + chr(tmp); intg := intg shr 8; end; end; function MakeOSCInt(value: Integer): String; var tmp: Byte; i, val: Integer; begin result := ''; {$IFDEF FPC} val := BEtoN(value); {$ELSE} val := htonl(value); {$ENDIF} for i := 0 to 3 do begin tmp := val and $ff; result := result + chr(tmp); val := val shr 8; end; end; function MakeOSCString(value: String): String; var i, ln: Integer; begin result := value; ln := 4 - (length(value)) mod 4; for i := 0 to ln - 1 do result := result + #0; end; function UnpackFloat(Bytes: PByte; var Offset: Integer): Single; var i, value: Integer; tmp: PByte; begin value := 0; tmp := Bytes; Inc(tmp, Offset); for i := 0 to 3 do begin value := value + tmp^ shl (i * 8); Inc(tmp); end; Inc(Offset, 4); {$IFDEF FPC} value := NtoBE(value); {$ELSE} value := ntohl(value); {$ENDIF} Result := Single(Pointer(value)); end; function UnpackInt(Bytes: PByte; var Offset: Integer): Integer; var i, value: Integer; tmp: PByte; begin value := 0; tmp := Bytes; Inc(tmp, Offset); for i := 0 to 3 do begin value := value + tmp^ shl (i * 8); Inc(tmp); end; Inc(Offset, 4); {$IFDEF FPC} Result := NtoBE(value); {$ELSE} Result := ntohl(value); {$ENDIF} end; function UnpackString(Bytes: PByte; var Offset: Integer): string; var tmp: PByte; off: Integer; begin tmp := Bytes; Inc(tmp, Offset); Result := PChar(tmp); off := Length(PChar(tmp)); off := off + (4 - off mod 4); Inc(Offset, off) end; constructor TOSCMessage.Create(Address: string); begin FAddress := Address; Create(nil); end; constructor TOSCMessage.Create(Bytes: PByte); begin inherited; FTypeTags := ','; FArguments := TStringList.Create; FIsDecoded := false; end; destructor TOSCMessage.Destroy; begin FArguments.Free; inherited; end; function TOSCMessage.AddAsString(TypeTag: Char; Value: String): HResult; begin Result := OSC_OK; try if TypeTag = 'f' then FArguments.Add(MakeOSCFloat(StrToFloat(Value))) else if TypeTag = 'i' then FArguments.Add(MakeOSCInt(StrToInt(Value))) else if TypeTag = 's' then FArguments.Add(MakeOSCString(Value)) else Result := OSC_UNRECOGNIZED_TYPETAG; except on EConvertError do Result := OSC_CONVERT_ERROR; end; if Result = OSC_OK then FTypeTags := FTypeTags + TypeTag; end; procedure TOSCMessage.AddFloat(Value: Single); begin FTypeTags := FTypeTags + 'f'; FArguments.Add(MakeOSCFloat(Value)); end; procedure TOSCMessage.AddInteger(Value: Integer); begin FTypeTags := FTypeTags + 'i'; FArguments.Add(MakeOSCInt(Value)); end; procedure TOSCMessage.AddString(Value: String); begin FTypeTags := FTypeTags + 's'; FArguments.Add(MakeOSCString(Value)); end; procedure TOSCMessage.Decode; var i, offset: Integer; begin if FIsDecoded then exit; offset := FTypeTagOffset; FTypeTags := UnpackString(FBytes, offset); for i := 1 to Length(FTypeTags) - 1 do begin if FTypeTags[i+1] = 's' then FArguments.Add(UnpackString(FBytes, offset)) else if FTypeTags[i+1] = 'i' then FArguments.Add(IntToStr(UnpackInt(FBytes, offset))) else if FTypeTags[i+1] = 'f' then FArguments.Add(FloatToStr(UnpackFloat(FBytes, offset))); end; FIsDecoded := true; end; function TOSCMessage.GetArgument(Index: Integer): string; begin Result := FArguments[Index]; end; function TOSCMessage.GetArgumentCount: Integer; begin Result := FArguments.Count; end; function TOSCMessage.GetTypeTag(Index: Integer): string; begin Result := FTypeTags[Index + 2]; end; function TOSCMessage.MatchAddress(Address: String): TOSCMessage; begin if not FMatched and MatchPattern(PChar(FAddress), PChar(Address)) then begin FMatched := true; Result := Self end else Result := nil; end; function TOSCMessage.ToOSCString: string; var i: Integer; begin Result := MakeOSCString(FAddress) + MakeOSCString(FTypeTags); for i := 0 to FArguments.Count - 1 do Result := Result + FArguments[i]; end; procedure TOSCMessage.Unmatch; begin FMatched := false; end; class function TOSCMessage.Unpack(Bytes: PByte; PacketOffset, Count: Integer; TimeTag: Extended = 0): TOSCPacket; begin Result := TOSCMessage.Create(Bytes); //for now decode address only (Result as TOSCMessage).Address := UnpackString(Bytes, PacketOffset); (Result as TOSCMessage).TimeTag := TimeTag; //save offset for later decoding on demand (Result as TOSCMessage).TypeTagOffset := PacketOffset; (Result as TOSCMessage).IsDecoded := false; end; constructor TOSCBundle.Create(Bytes: PByte); begin inherited; FPackets := TObjectList.Create; FPackets.OwnsObjects := true; end; destructor TOSCBundle.Destroy; begin FPackets.Free; inherited; end; procedure TOSCBundle.Add(const Packet: TOSCPacket); begin FPackets.Add(Packet); end; function TOSCBundle.MatchAddress(Address: String): TOSCMessage; var i: Integer; begin Result := nil; for i := 0 to FPackets.Count - 1 do begin Result := (FPackets[i] as TOSCPacket).MatchAddress(Address); if Assigned(Result) then break; end; end; function TOSCBundle.ToOSCString: string; var timeTag: String; i: Integer; packet: String; begin timeTag := #0#0#0#0#0#0#0#1; //immediately Result := MakeOSCString('#bundle') + timeTag; for i := 0 to FPackets.Count - 1 do begin packet := (FPackets[i] as TOSCPacket).ToOSCString; Result := Result + MakeOSCInt(Length(packet)) + packet; end; end; procedure TOSCBundle.Unmatch; var i: Integer; begin for i := 0 to FPackets.Count - 1 do (FPackets[i] as TOSCPacket).UnMatch; end; class function TOSCBundle.Unpack(Bytes: PByte; PacketOffset, Count: Integer; TimeTag: Extended = 0): TOSCPacket; var packetLength: Integer; tt1, tt2: Cardinal; begin Result := TOSCBundle.Create(Bytes); //advance the '#bundle' string UnpackString(Bytes, PacketOffset); //advance the timestamp tt1 := Cardinal(UnpackInt(Bytes, PacketOffset)); tt2 := Cardinal(UnpackInt(Bytes, PacketOffset)); TimeTag := tt1 + tt2 / power(2, 32); while PacketOffset < Count do begin packetLength := UnpackInt(Bytes, PacketOffset); //note: PacketOffset is always from the very beginning of Bytes! //not the beginning of the current packet. (Result as TOSCBundle).Add(TOSCPacket.Unpack(Bytes, PacketOffset, PacketOffset + packetLength, TimeTag)); Inc(PacketOffset, packetLength); end; end; constructor TOSCPacket.Create(Bytes: PByte); begin FBytes := Bytes; end; // we know that pattern[0] == '[' and test[0] != 0 */ function TOSCPacket.MatchBrackets(pMessage, pAddress: PChar): Boolean; var negated: Boolean; p, p1, p2: PChar; begin p := pMessage; Result := false; negated := false; Inc(pMessage); if pMessage^ = #0 then begin //LogWarningFMT('Unterminated [ in message: %s', [FInput[0]]); Dec(pMessage); exit; end; if pMessage^ = '!' then begin negated := true; Inc(p); end; Dec(pMessage); Result := negated; while p^ <> ']' do begin if p^ = #0 then begin //LogWarningFMT('Unterminated [ in message: %s', [FInput[0]]); exit; end; p1 := p + 1; // sizeOf(PChar); p2 := p1 + 1; //sizeOf(PChar); if (p1^ = '-') and (p2^ <> #0) then if (Ord(pAddress^) >= Ord(p^)) and (Ord(pAddress^) <= Ord(p2^)) then begin Result := not negated; break; end; if p^ = pAddress^ then begin Result := not negated; break; end; Inc(p); end; if Result = false then exit; while p^ <> ']' do begin if p^ = #0 then begin //LogWarningFMT('Unterminated [ in message: %s', [FInput[0]]); exit; end; Inc(p); end; Inc(p); pMessage := p; Inc(pAddress); Result := MatchPattern(p, pAddress); end; function TOSCPacket.MatchList(pMessage, pAddress: PChar): Boolean; var p, tp: PChar; begin Result := false; p := pMessage; tp := pAddress; while p^ <> '}' do begin if p^ = #0 then begin //LogWarningFMT('Unterminated { in message: %s', [FInput[0]]); exit; end; Inc(p); end; // for(restOfPattern = pattern; *restOfPattern != '}'; restOfPattern++) { // if (*restOfPattern == 0) { // OSCWarning("Unterminated { in pattern \".../%s/...\"", theWholePattern); // return FALSE; // } //} Inc(p); // skip close curly brace Inc(pMessage); // skip open curly brace while true do begin if pMessage^ = ',' then begin if MatchPattern(p, tp) then begin Result := true; pMessage := p; pAddress := tp; exit; end else begin tp := pAddress; Inc(pMessage); end; end else if pMessage^ = '}' then begin Result := MatchPattern(p, tp); pMessage := p; pAddress := tp; exit; end else if pMessage^ = tp^ then begin Inc(pMessage); Inc(tp); end else begin tp := pAddress; while (pMessage^ <> ',') and (pMessage^ <> '}') do Inc(pMessage); if pMessage^ = ',' then Inc(pMessage); end; end; end; function TOSCPacket.MatchPattern(pMessage, pAddress: PChar): Boolean; begin if (pMessage = nil) or (pMessage^ = #0) then begin Result := pAddress^ = #0; exit; end; if pAddress^ = #0 then begin if pMessage^ = '*' then begin Result := MatchPattern(pMessage + 1, pAddress); exit; end else begin Result := false; exit; end; end; case pMessage^ of #0 : Result := pAddress^ = #0; '?': Result := MatchPattern(pMessage + 1, pAddress + 1); '*': begin if MatchPattern(pMessage + 1, pAddress) then Result := true else Result := MatchPattern(pMessage, pAddress + 1); end; ']','}': begin //LogWarningFMT('Spurious %s in message: %s', [pMessage^, FInput[0]]); Result := false; end; '[': Result := MatchBrackets(pMessage, pAddress); '{': Result := MatchList(pMessage, pAddress); {'\\': begin if pMessage^ + 1 = #0 then Result := pAddress^ = #0 else if pMessage^ + 1 = pAddress^ Result := MatchPattern(pMessage + 2, pAddress + 1) else Result := false; end; } else if pMessage^ = pAddress^ then Result := MatchPattern(pMessage + 1,pAddress + 1) else Result := false; end; end; class function TOSCPacket.Unpack(Bytes: PByte; Count: Integer): TOSCPacket; begin Result := UnPack(Bytes, 0, Count); end; class function TOSCPacket.Unpack(Bytes: PByte; Offset, Count: Integer; TimeTag: Extended = 0): TOSCPacket; var tmp: PByte; begin tmp := Bytes; Inc(tmp, Offset); if Char(tmp^) = '#' then Result := TOSCBundle.UnPack(Bytes, Offset, Count) else Result := TOSCMessage.UnPack(Bytes, Offset, Count, TimeTag); end; end.