-module(osc). %% -- %% This file is part of Sonic Pi: http://sonic-pi.net %% Full project source: https://github.com/samaaron/sonic-pi %% License: https://github.com/samaaron/sonic-pi/blob/master/LICENSE.md %% %% Copyright 2016,2017 by Joe Armstrong (http://joearms.github.io/) %% All rights reserved. %% %% Permission is granted for use, copying, modification, and %% distribution of modified versions of this work as long as this %% notice is included. %% ++ -export([encode/1, decode/1, test0/0, test1/0, test2/0, now/0, osc_time_to_local/1, pack_ts/2, encode_time/1]). %% Note: not all tags are implemented yet %% note super well tested - appears to work :-) %%---------------------------------------------------------------------- %% Encoding %%---------------------------------------------------------------------- %% Do do check endian %% I've said Int64 are unsigned-little-integers %% I think they should be big %% The OSC spec is unclear about this point %% To do check endianness carefully %% To do add device number test0() -> B = encode(["/mi",{int64, 347873045749854},145,53,0]), io:format("B=~p~n",[B]), D = (catch decode(B)), io:format("D=~p~n",[D]). test1() -> decode(<<35,98,117,110,100,108,101,0,218,114,254,188,137,88,216,0,0,0,0,16, 47,102,111,111,0,0,0,0,44,115,0,0,98,97,114,0>>). test2() -> pack_ts(osc:now() + 10, ["/forward", "localhost", 6000, "/sendmidi", 12, 34, 56]). %% osc:now() returns the system time as a float %% The units of the timestamp are seconds past epoch now() -> %% nanoseconds past epoc erlang:system_time(nanosecond)/1000000000. osc_time_to_local(Tsec) -> Native = trunc(Tsec*1000000000), %% 9 zeros Nano = erlang:convert_time_unit(Native, native, micro_seconds), S = Nano div 1000000000, %% 9 zeros %% the three args below are MegaSec Sec MicroSec Micro = Nano rem 1000000, %%6 Time = calendar:now_to_local_time({S div 1000000000, S rem 1000000000, Micro}), Frac = Micro/1000000, %% Umm not totally convinced of this ... {Time, Frac}. encode([Verb|Args]) -> %% io:format(Verb), %% io:format(Args), %% io:format("===="), Str = encode_string(Verb), Flags = encode_flags(Args), Data = [encode_arg(I) || I <- Args], list_to_binary([Str,Flags,Data]). encode_string(S) -> %% zero terminate S and pad to 4 byte boundary case length(S) rem 4 of 0 -> [S,0,0,0,0]; 1 -> [S,0,0,0]; 2 -> [S,0,0]; 3 -> [S,0] end. encode_binary(S) -> %% pad to 4 byte boundary case length(S) rem 4 of 0 -> [S]; 1 -> [S, 0, 0, 0]; 2 -> [S, 0, 0]; 3 -> [S, 0] end. encode_flags(L) when is_list(L) -> %% flags starts with , and is terminated with a zero %% so it's really a string :-) L1 = [encode_flag(I) || I <- L], encode_string([$,|L1]). encode_flag({int64,_}) -> $h; encode_flag({time,_}) -> $t; encode_flag(I) when is_integer(I) -> $i; encode_flag(X) when is_list(X) -> $s; encode_flag(X) when is_atom(X) -> $s; encode_flag(X) when is_float(X) -> $f; encode_flag(X) when is_binary(X) -> $b. encode_arg(X) when is_list(X) -> encode_string(X); encode_arg(X) when is_atom(X) -> encode_string(atom_to_list(X)); encode_arg(X) when is_integer(X) -> <>; encode_arg(X) when is_float(X) -> <>; % encode_arg({int64,X}) -> <>; encode_arg(X) when is_binary(X) -> BinList = binary:bin_to_list(X), L = length(BinList), Length = <>, Data = encode_binary(BinList), lists:flatten([Length, Data]). %% bundles pack_ts(Time, Data) -> %% io:format("Pack ts:~p ~p~n", [Time, Data]), %% Time is an NTP timestamp BTime = encode_time(Time), BData = encode(Data), Size = size(BData), B = <<"#bundle",0,BTime/binary,Size:32,BData/binary>>, %% uppack just to check %% {bundle,T1,[{_,B1}]} = decode(B), %% E1 = decode(B1), %% io:format("decoded:~p ~p~n",[T1,E1]), B. %%---------------------------------------------------------------------- %5 Decoding %%---------------------------------------------------------------------- decode(B0) when is_binary(B0) -> {Verb, B1} = get_string(B0), %% io:format("Verb: ~p~n",[Verb]), case Verb of "#bundle" -> <> = B1, {bundle, decode_time(Time), decode_bundle(B2)}; _ -> {[$,|Flags], B2} = get_string(B1), %% io:format("Verb: ~p Flags:~p~n",[Verb,Flags]), {cmd, [Verb|get_args(Flags, B2, [])]} end. -define(EPOCH, 2208988800). % offset yr 1900 to unix epoch %% encode_time(Time) %% Time is inlocal units - for example %% Time = osc:now() + 10.0 means (in 10 seconds time) encode_time(Time) -> T1 = Time + ?EPOCH, IntPart = trunc(T1), F = T1 - IntPart, FracPart = trunc(F * (2 bsl 31)), <>. decode_time(<>) -> X - ?EPOCH + binfrac(Y). %% binfrac(Bin) -> binfrac(Bin, 2, 0). %% binfrac(0, _, Frac) -> Frac; %% binfrac(Bin, N, Frac) -> binfrac(Bin bsr 1, N*2, Frac + (Bin band 1)/N). binfrac(I) -> I / (2 bsl 31). decode_bundle(<>) -> [{Size, B}|decode_bundle(B1)]; decode_bundle(<<>>) -> []. get_args([$i|T1], <>, L) -> get_args(T1, T2, [I|L]); get_args([$f|T1], <>, L) -> get_args(T1, T2, [F|L]); get_args([$h|T1], <>, L) -> get_args(T1, T2, [{int64,I}|L]); get_args([$d|T1], <>, L) -> get_args(T1, T2, [Double|L]); get_args([$s|T1], B0, L) -> {Str, B1} = get_string(B0), get_args(T1, B1, [Str|L]); get_args([], _, L) -> lists:reverse(L). get_string(X) when is_binary(X) -> [Bin,After] = binary:split(X, <<0>>), %% skip to bounday K = 3 - (size(Bin) rem 4), {binary_to_list(Bin), skip(K, After)}. skip(0, B) -> B; skip(N, B) -> element(2, erlang:split_binary(B, N)).