├── .gitignore ├── Emakefile ├── README ├── ebin └── osc.app ├── src ├── osc.erl ├── osc_methods.erl ├── osc_app.erl ├── osc_sup.erl ├── osc_server.erl └── osc_lib.erl ├── Makefile ├── doc └── overview.edoc ├── LICENSE └── bin └── oscd /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.beam 3 | -------------------------------------------------------------------------------- /Emakefile: -------------------------------------------------------------------------------- 1 | %% -*- Erlang -*- 2 | {"src/*", [debug_info, {outdir, "ebin"}, {i, "include"}]}. 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This is an implementation of the Open Sound Control (OSC) protocol written 2 | in Erlang. 3 | 4 | For more information about OSC, see http://www.opensoundcontrol.org/ 5 | -------------------------------------------------------------------------------- /ebin/osc.app: -------------------------------------------------------------------------------- 1 | %% -*-Erlang-*- 2 | {application, osc, 3 | [{description, "Open Sound Control Application"}, 4 | {vsn, "1.0.0"}, 5 | {modules, [osc_app, osc_sup, osc_lib, osc_server, osc_methods]}, 6 | {registered, [osc_sup, osc_server]}, 7 | {applications, [kernel, stdlib]}, 8 | {mod, {osc_app, []}}, 9 | {env, [{port, 7123}, {recbuf, 8192}]} 10 | ]}. 11 | %% vim: set filetype=erlang : 12 | -------------------------------------------------------------------------------- /src/osc.erl: -------------------------------------------------------------------------------- 1 | %% @author Ruslan Babayev 2 | %% @copyright 2009 Ruslan Babayev 3 | 4 | -module(osc). 5 | -author('ruslan@babayev.com'). 6 | 7 | -export([start/0, stop/0]). 8 | 9 | %% @doc Starts the application. 10 | %% @spec start() -> ok | {error, Reason} 11 | start() -> 12 | application:start(osc). 13 | 14 | %% @doc Stops the application. 15 | %% @spec stop() -> ok 16 | stop() -> 17 | application:stop(osc). 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ERL=erl 2 | ERLC=erlc 3 | APP=osc 4 | 5 | all: compile 6 | 7 | compile: 8 | @$(ERL) -make 9 | 10 | clean: 11 | @echo "removing:" 12 | @rm -fv ebin/*.beam 13 | 14 | docs: 15 | @$(ERL) -noshell -run edoc_run application '$(APP)' '"."' '[]' 16 | 17 | clean-docs: 18 | @echo "removing:" 19 | @rm -fv doc/edoc-info doc/*.html doc/*.css doc/*.png 20 | 21 | run: compile 22 | @$(ERL) -pa ebin -s $(APP) 23 | 24 | test: compile 25 | @$(ERL) -pa ebin -eval "eunit:test({application,$(APP)})" \ 26 | -noshell -s init stop 27 | -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | @author Ruslan Babayev 2 | @copyright 2009 Ruslan Babayev 3 | @version 1.0.0 4 | @title Erlang Open Sound Control Application 5 | @reference OSC 1.0 6 | @reference OSC 1.1 7 | @doc This is an Erlang application that implements OSC 1.1 specification. 8 | 9 | Open Sound Control (OSC) is an open, transport-independent, message-based 10 | protocol developed for communication among computers, sound synthesizers, 11 | and other multimedia devices. 12 | -------------------------------------------------------------------------------- /src/osc_methods.erl: -------------------------------------------------------------------------------- 1 | %% @author Tobias Rodaebel 2 | %% @doc Adding OSC methods. 3 | 4 | -module(osc_methods). 5 | 6 | -export([add_methods/0, delete_methods/0, log_data/1]). 7 | 8 | -define(SERVER, {global, osc_server}). 9 | 10 | %% @doc Adds methods. 11 | %% @spec add_methods() -> ok 12 | add_methods() -> 13 | gen_server:cast(?SERVER, {add_method, "/1/stop", ?MODULE, log_data}). 14 | 15 | %% @doc Deletes methods. 16 | %% @spec delete_methods() -> ok 17 | delete_methods() -> 18 | gen_server:cast({global, osc_server}, {delete_method, "/1/stop"}). 19 | 20 | %% @doc Logs handled data. 21 | %% @spec log_data(Data) -> Data 22 | log_data(Data) -> 23 | error_logger:info_msg("Received ~p", [Data]), 24 | Data. 25 | -------------------------------------------------------------------------------- /src/osc_app.erl: -------------------------------------------------------------------------------- 1 | %% @author Ruslan Babayev 2 | %% @copyright 2009 Ruslan Babayev 3 | %% @doc OSC Application. 4 | 5 | -module(osc_app). 6 | -author('ruslan@babayev.com'). 7 | 8 | -behaviour(application). 9 | 10 | %% Application callbacks 11 | -export([start/2, stop/1]). 12 | 13 | %% @doc Starts the application. 14 | %% @spec start(StartType, StartArgs) -> {ok, Pid} | 15 | %% {ok, Pid, State} | 16 | %% {error, Reason} 17 | %% StartType = normal | {takeover, Node} | {failover, Node} 18 | %% StartArgs = term() 19 | start(_StartType, _StartArgs) -> 20 | osc_sup:start_link(). 21 | 22 | %% @doc Stops the application. 23 | %% @spec stop(State) -> ok 24 | stop(_State) -> 25 | ok. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Ruslan Babayev 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/osc_sup.erl: -------------------------------------------------------------------------------- 1 | %% @author Ruslan Babayev 2 | %% @copyright 2009 Ruslan Babayev 3 | %% @doc OSC Supervisor 4 | 5 | -module(osc_sup). 6 | -author('ruslan@babayev.com'). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | %% @doc Starts the supervisor. 17 | %% @spec start_link() -> {ok, Pid} | ignore | {error, Reason} 18 | start_link() -> 19 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 20 | 21 | %% @doc Initializes the supervisor. 22 | %% @spec init(Args) -> {ok, {SupFlags, ChildSpecs}} | ignore | {error, Reason} 23 | init([]) -> 24 | Server = {osc_server, {osc_server, start_link, []}, 25 | permanent, 2000, worker, [osc_server]}, 26 | {ok, {{one_for_one, 3, 10}, [Server]}}. 27 | -------------------------------------------------------------------------------- /bin/oscd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd `dirname $0`; 3 | CWD=`dirname "$PWD" | sed -e 's, ,\\\\ ,g'`; 4 | cd "$PWD"; 5 | BASENAME=`basename $0` 6 | MONITOR="false" 7 | DETACHED_OPTION="" 8 | COOKIE="cookie" 9 | ERL=erl 10 | 11 | usage() { 12 | echo "Usage: $BASENAME [options]"; 13 | } 14 | 15 | for _option 16 | do 17 | # If the previous option needs an argument, assign it. 18 | if test -n "$_prev"; then 19 | eval "$_prev=\$_option" 20 | _prev= 21 | continue 22 | fi 23 | 24 | case "$_option" in 25 | -*=*) _optarg=`echo "$_option" | sed 's/[-_a-zA-Z0-9]*=//'` ;; 26 | *) _optarg= ;; 27 | esac 28 | 29 | case "$_option" in 30 | 31 | --cookie=*) 32 | COOKIE="$_optarg" ;; 33 | 34 | --detached | -d) 35 | DETACHED_OPTION="-detached" ;; 36 | 37 | --help | -h) 38 | usage 39 | cat << EOF 40 | 41 | Options: 42 | --cookie=COOKIE use this cookie 43 | -d,--detached starts the service detached from the system console 44 | -h,--help display this message 45 | -m,--monitor run monitor 46 | 47 | EOF 48 | exit 0;; 49 | 50 | --monitor | -m) 51 | MONITOR="true" ;; 52 | 53 | --* | -* ) { 54 | echo "$BASENAME: illegal option $_option; use --help to show usage" 1>&2; 55 | exit 1; 56 | };; 57 | * ) 58 | args="$args$_option " ;; 59 | 60 | esac 61 | done 62 | 63 | ERL_OPTS=" 64 | -setcookie $COOKIE \ 65 | -pa $CWD/ebin \ 66 | -sname osc \ 67 | -s osc" 68 | 69 | if [ "$MONITOR" = "false" ]; then 70 | eval $ERL $ERL_OPTS $DETACHED_OPTION start -noshell 71 | else 72 | eval $ERL $ERL_OPTS -boot start_sasl -run appmon start 73 | fi 74 | 75 | exit 0; 76 | -------------------------------------------------------------------------------- /src/osc_server.erl: -------------------------------------------------------------------------------- 1 | %% @author Ruslan Babayev 2 | %% @copyright 2009 Ruslan Babayev 3 | %% @doc OSC Server. 4 | 5 | -module(osc_server). 6 | -author("ruslan@babayev.com"). 7 | -vsn("1.0.0"). 8 | 9 | -behaviour(gen_server). 10 | 11 | %% API 12 | -export([start_link/0, add_method/3, delete_method/1]). 13 | 14 | %% gen_server callbacks 15 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 16 | terminate/2, code_change/3]). 17 | 18 | -define(SERVER, ?MODULE). 19 | 20 | -record(state, {socket, methods}). 21 | 22 | %% @doc Starts the server. 23 | %% @spec start_link() -> {ok, Pid} | ignore | {error, Reason} 24 | start_link() -> 25 | gen_server:start_link({global, ?SERVER}, ?MODULE, [], []). 26 | 27 | %% @doc Adds a method. 28 | %% @spec add_method(string(), atom(), atom()) -> ok 29 | add_method(Address, Module, Function) -> 30 | gen_server:cast(?SERVER, {add_method, Address, Module, Function}). 31 | 32 | %% @doc Deletes a method. 33 | %% @spec delete_method(Address) -> ok 34 | delete_method(Address) -> 35 | gen_server:cast(?SERVER, {delete_method, Address}). 36 | 37 | %% @private 38 | %% @doc Initializes the server. 39 | %% @spec init(Args) -> {ok, State} | 40 | %% {ok, State, Timeout} | 41 | %% ignore | 42 | %% {stop, Reason} 43 | init([]) -> 44 | {ok, Port} = application:get_env(port), 45 | {ok, RecBuf} = application:get_env(recbuf), 46 | Options = [binary, {active, once}, {recbuf, RecBuf}], 47 | case gen_udp:open(Port, Options) of 48 | {ok, Socket} -> 49 | Methods = ets:new(osc_methods, [named_table, ordered_set]), 50 | {ok, #state{socket = Socket, methods = Methods}}; 51 | {error, Reason} -> 52 | error_logger:error_report({?MODULE, udp_open, Reason}), 53 | {stop, {?MODULE, udp_open, Reason}} 54 | end. 55 | 56 | %% @private 57 | %% @doc Handles call messages. 58 | %% @spec handle_call(Request, From, State) -> 59 | %% {reply, Reply, State} | 60 | %% {reply, Reply, State, Timeout} | 61 | %% {noreply, State} | 62 | %% {noreply, State, Timeout} | 63 | %% {stop, Reason, Reply, State} | 64 | %% {stop, Reason, State} 65 | handle_call(_Request, _From, State) -> 66 | Reply = ok, 67 | {reply, Reply, State}. 68 | 69 | %% @private 70 | %% @doc Handles cast messages. 71 | %% @spec handle_cast(Msg, State) -> {noreply, State} | 72 | %% {noreply, State, Timeout} | 73 | %% {stop, Reason, State} 74 | handle_cast({add_method, Address, Module, Function}, State) -> 75 | Methods = State#state.methods, 76 | ets:insert(Methods, {string:tokens(Address, "/"), {Module, Function}}), 77 | {noreply, State}; 78 | handle_cast({delete_method, Address}, State) -> 79 | Methods = State#state.methods, 80 | ets:delete(Methods, string:tokens(Address, "/")), 81 | {noreply, State}. 82 | 83 | %% @private 84 | %% @doc Handles all non call/cast messages. 85 | %% @spec handle_info(Info, State) -> {noreply, State} | 86 | %% {noreply, State, Timeout} | 87 | %% {stop, Reason, State} 88 | handle_info({udp, Socket, _IP, _Port, Packet}, State) -> 89 | inet:setopts(Socket, [{active, once}]), 90 | Methods = State#state.methods, 91 | try osc_lib:decode(Packet) of 92 | {message, Address, Args} -> 93 | handle_message(immediately, Address, Args, Methods); 94 | {bundle, When, Elements} -> 95 | handle_bundle(When, Elements, Methods) 96 | catch 97 | Class:Term -> 98 | error_logger:error_report({osc_lib,decode,Class,Term}) 99 | end, 100 | {noreply, State}; 101 | handle_info(_Info, State) -> 102 | {noreply, State}. 103 | 104 | %% @private 105 | %% @doc Performs cleanup on termination. 106 | %% @spec terminate(Reason, State) -> void() 107 | terminate(_Reason, State) -> 108 | gen_udp:close(State#state.socket), 109 | ok. 110 | 111 | %% @private 112 | %% @doc Converts process state when code is changed. 113 | %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} 114 | code_change(_OldVsn, State, _Extra) -> 115 | {ok, State}. 116 | 117 | %% @doc Handles OSC messages. 118 | %% @spec handle_message(When, Address, Args, Methods) -> any() 119 | %% When = time() 120 | %% Address = string() 121 | %% Args = [any()] 122 | %% Methods = [method()] 123 | handle_message(When, Address, Args, Methods) -> 124 | case ets:match(Methods, make_pattern(Address)) of 125 | [] -> 126 | error_logger:info_report({unhandled,{message,Address,Args}}); 127 | Matches -> 128 | Time = when_to_millisecs(When), 129 | [timer:apply_after(Time, Module, Function, Args) || 130 | [{Module, Function}] <- Matches] 131 | end. 132 | 133 | %% @doc Converts the OSC address pattern into ETS match spec. 134 | %% @spec make_pattern(string()) -> tuple() 135 | make_pattern(Address) -> 136 | make_pattern(string:tokens(Address, "/"), []). 137 | 138 | make_pattern([], Acc) -> 139 | {lists:reverse(Acc), '$1'}; 140 | make_pattern(["*"], Acc) -> 141 | {lists:reverse(Acc) ++ '_', '$1'}; 142 | make_pattern([H|T], Acc) -> 143 | make_pattern(T, [make_pattern2(H, [])|Acc]). 144 | 145 | make_pattern2([], Acc) -> 146 | lists:reverse(Acc); 147 | make_pattern2([$*|_], Acc) -> 148 | lists:reverse(Acc) ++ '_'; 149 | make_pattern2([$?|T], Acc) -> 150 | make_pattern2(T, ['_'|Acc]); 151 | make_pattern2([H|T], Acc) -> 152 | make_pattern2(T, [H|Acc]). 153 | 154 | %% @doc Converts OSC time to milliseconds. 155 | %% @spec when_to_millisecs(When) -> integer() 156 | %% When = immediately | {time, Seconds::integer(), Fractions::integer()} 157 | when_to_millisecs(immediately) -> 158 | 0; 159 | when_to_millisecs({time, Seconds, Fractions}) -> 160 | {MegaSecs, Secs, MicroSecs} = now(), 161 | S = (Seconds - 2208988800) - (MegaSecs * 1000000 + Secs), 162 | F = Fractions - (MicroSecs * 1000000), 163 | case (S * 1000) + (1000 div F) of 164 | Time when Time > 0 -> 165 | Time; 166 | _ -> 167 | 0 168 | end. 169 | 170 | %% @doc Handles OSC bundles. 171 | %% @spec handle_bundle(When, Elements, Methods) -> any() 172 | %% Elements = [message() | bundle()] 173 | %% Methods = [method()] 174 | %% message() = {message, Address::string(), Args::[any()]} 175 | %% bundle() = {bundle, When::time(), [message() | bundle()]} 176 | %% time() = immediately | {time, Seconds::integer(), Fractions::integer()} 177 | %% method() = {Module::atom(), Function::atom()} 178 | handle_bundle(_When, [], _Methods) -> 179 | ok; 180 | handle_bundle(When, [{message, Address, Args} | Rest], Methods) -> 181 | handle_message(When, Address, Args, Methods), 182 | handle_bundle(When, Rest, Methods); 183 | handle_bundle(When, [{bundle, InnerWhen, Elements} | Rest], Methods) -> 184 | handle_bundle(InnerWhen, Elements, Methods), 185 | handle_bundle(When, Rest, Methods). 186 | -------------------------------------------------------------------------------- /src/osc_lib.erl: -------------------------------------------------------------------------------- 1 | %% @author Ruslan Babayev 2 | %% @author Tobias Rodaebel 3 | %% @copyright 2009 Ruslan Babayev 4 | %% @doc OSC Decoding/Encoding Library. 5 | 6 | -module(osc_lib). 7 | -author("ruslan@babayev.com"). 8 | -author("tobias.rodaebel@googlemail.com"). 9 | 10 | -export([decode/1, encode/1]). 11 | 12 | -include_lib("eunit/include/eunit.hrl"). 13 | 14 | %% @type message() = {message, Address::string(), args()} 15 | %% @type args() = [integer() | float() | binary() | time() | atom() | time() | 16 | %% rgba() | midi() | true | false | null | impulse | args()] 17 | %% @type time() = immediately | {time, Seconds::integer(), Fractions::integer()} 18 | %% @type rgba() = {rgba, R::integer(), G::integer(), B::integer(), A::integer()} 19 | %% @type midi() = {midi, Port::integer(), Status::integer(), binary(), binary()} 20 | %% @type bundle() = {bundle, When::time(), [message() | bundle()]} 21 | 22 | %% @doc Decodes messages. 23 | %% @spec decode(Bytes::binary()) -> message() | bundle() 24 | decode(<<"#bundle", 0, Time:8/binary, Rest/binary>>) -> 25 | {bundle, decode_time(Time), decode_bundle_elems(Rest, [])}; 26 | decode(<<"/", _/binary>> = Bin) -> 27 | {Address, Rest1} = decode_string(Bin), 28 | {Args, _} = 29 | try decode_string(Rest1) of 30 | {[$,|Types], Rest2} -> 31 | decode_args(Rest2, Types); 32 | _ -> 33 | {[Rest1], <<>>} 34 | catch 35 | _:_ -> 36 | {[Rest1], <<>>} 37 | end, 38 | {message, Address, Args}. 39 | 40 | %% @doc Decodes bundle elements. 41 | %% @spec decode_bundle_elems(Bytes::binary(), list()) -> [message() | bundle()] 42 | decode_bundle_elems(<<>>, Acc) -> 43 | lists:reverse(Acc); 44 | decode_bundle_elems(<>, Acc) -> 45 | decode_bundle_elems(Rest, [decode(Bin)|Acc]). 46 | 47 | %% @doc Decodes times. 48 | %% @spec decode_time(Bytes::binary()) -> time() 49 | decode_time(<<1:64>>) -> 50 | immediately; 51 | decode_time(<>) -> 52 | {time, Seconds, Fractions}. 53 | 54 | %% @doc Decodes a padded and zero-terminated string. 55 | %% @spec decode_string(Bytes::binary()) -> {String::string(), Rest::binary()} 56 | decode_string(Bin) -> 57 | decode_string(Bin, []). 58 | 59 | %% @doc Decodes a padded and zero-terminated string. 60 | %% @spec decode_string(Bytes::binary(), string()) -> {string(), binary()} 61 | decode_string(<<0, Rest/binary>>, Acc) -> 62 | L = pad_len(length(Acc) + 1, 4), 63 | <<_:L/integer-unit:8, Rest1/binary>> = Rest, 64 | {lists:reverse(Acc), Rest1}; 65 | decode_string(<>, Acc) -> 66 | decode_string(Rest, [Byte|Acc]). 67 | 68 | %% @hidden 69 | decode_strings_test_() -> 70 | [?_assertEqual({"hello", <<>>}, decode_string(<<"hello",0,0,0>>)), 71 | ?_assertEqual({"hello1", <<>>}, decode_string(<<"hello1",0,0>>)), 72 | ?_assertEqual({"hello12", <<>>}, decode_string(<<"hello12",0>>)), 73 | ?_assertEqual({"hello123", <<>>}, decode_string(<<"hello123",0,0,0,0>>))]. 74 | 75 | %% @doc Zero-pads the binary. 76 | %% @spec pad(Bytes::binary(), Pad::integer()) -> binary() 77 | pad(B, P) when is_binary(B), is_integer(P) -> 78 | L = pad_len(size(B), P), 79 | <>. 80 | 81 | %% @doc Returns the length the binary has to be padded by. 82 | %% @spec pad_len(Length::binary(), Padding::integer()) -> integer() 83 | pad_len(L, P) when L rem P == 0 -> 84 | 0; 85 | pad_len(L, P) -> 86 | P - (L rem P). 87 | 88 | %% @doc Decodes a BLOB. 89 | %% @spec decode_blob(Bytes::binary()) -> {Blob::binary(), Rest::binary()} 90 | decode_blob(<>) -> 91 | L = pad_len(Length + 4, 4), 92 | <<_:L/integer-unit:8, Rest1/binary>> = Rest, 93 | {Bytes, Rest1}. 94 | 95 | %% @hidden 96 | decode_blobs_test_() -> 97 | [?_assertEqual({<<>>, <<>>}, decode_blob(<<0,0,0,0>>)), 98 | ?_assertEqual({<<1>>, <<>>}, decode_blob(<<0,0,0,1,1,0,0,0>>)), 99 | ?_assertEqual({<<1,2>>, <<>>}, decode_blob(<<0,0,0,2,1,2,0,0>>)), 100 | ?_assertEqual({<<1,2,3>>, <<>>}, decode_blob(<<0,0,0,3,1,2,3,0>>)), 101 | ?_assertEqual({<<1,2,3,4>>, <<>>}, decode_blob(<<0,0,0,4,1,2,3,4>>))]. 102 | 103 | %% @doc Decodes arguments. 104 | %% @spec decode_args(Bytes::binary(), Types::string()) -> args() 105 | decode_args(Bin, Types) -> 106 | decode_args(Bin, Types, []). 107 | 108 | %% @doc Decodes arguments. 109 | %% @spec decode_args(Bytes::binary(), Types::string(), Acc::args()) -> args() 110 | decode_args(Rest, [], Acc) -> 111 | {lists:reverse(Acc), Rest}; 112 | decode_args(<>, [$i|T], Acc) -> 113 | decode_args(Rest, T, [Int32|Acc]); 114 | decode_args(<>, [$f|T], Acc) -> 115 | decode_args(Rest, T, [Float|Acc]); 116 | decode_args(Bin, [$s|T], Acc) -> 117 | {String, Rest} = decode_string(Bin), 118 | decode_args(Rest, T, [String|Acc]); 119 | decode_args(Bin, [$b|T], Acc) -> 120 | {Blob, Rest} = decode_blob(Bin), 121 | decode_args(Rest, T, [Blob|Acc]); 122 | decode_args(<>, [$h|T], Acc) -> 123 | decode_args(Rest, T, [Int64|Acc]); 124 | decode_args(<>, [$t|T], Acc) -> 125 | decode_args(Rest, T, [decode_time(Time)|Acc]); 126 | decode_args(<>, [$d|T], Acc) -> 127 | decode_args(Rest, T, [Double|Acc]); 128 | decode_args(Bin, [$S|T], Acc) -> 129 | {Symbol, Rest} = decode_string(Bin), 130 | decode_args(Rest, T, [list_to_atom(Symbol)|Acc]); 131 | decode_args(<>, [$c|T], Acc) -> 132 | decode_args(Rest, T, [Char|Acc]); 133 | decode_args(<>, [$r|T], Acc) -> 134 | decode_args(Rest, T, [{rgba,R,G,B,A}|Acc]); 135 | decode_args(<>, [$m|T], Acc) -> 136 | decode_args(Rest, T, [{midi,Port,Status,Data1,Data2}|Acc]); 137 | decode_args(Bin, [$T|T], Acc) -> 138 | decode_args(Bin, T, [true|Acc]); 139 | decode_args(Bin, [$F|T], Acc) -> 140 | decode_args(Bin, T, [false|Acc]); 141 | decode_args(Bin, [$N|T], Acc) -> 142 | decode_args(Bin, T, [null|Acc]); 143 | decode_args(Bin, [$I|T], Acc) -> 144 | decode_args(Bin, T, [impulse|Acc]); 145 | decode_args(Bin, [$[|T], Acc) -> 146 | {Array, RestBin, RestTypes} = decode_args(Bin, T, []), 147 | decode_args(RestBin, RestTypes, [Array|Acc]); 148 | decode_args(Bin, [$]|T], Acc) -> 149 | {lists:reverse(Acc), Bin, T}. 150 | 151 | %% @hidden 152 | decode_args_test() -> 153 | Bin = <<1:32,102,111,111,0,1:32,4:32,5:32,2:32,3:32,255,255,255,255>>, 154 | Types = "is[i[ii]i]ir", 155 | Args = [1,"foo",[1,[4,5],2],3,{rgba, 255,255,255,255}], 156 | ?assertEqual({Args, <<>>}, decode_args(Bin, Types, [])). 157 | 158 | %% @doc Encodes messages. 159 | %% @spec encode(Data::message()|bundle()) -> Bytes::binary() 160 | encode({bundle, Time, Elems}) -> 161 | Bin = [[<<"#bundle",0>>,encode_time(Time)], encode_bundle_elems(Elems, [])], 162 | list_to_binary(Bin); 163 | encode({message, Address, Args}) -> 164 | Bytes = encode_string(Address), 165 | {Data, Types} = encode_args(Args), 166 | list_to_binary([<>,[encode_types(Types, []),Data]]). 167 | 168 | %% @hidden 169 | encode_test_() -> 170 | Message1 = {message, "/1/play", [1.0]}, 171 | Result1 = <<47,49,47,112,108,97,121,0,44,102,0,0,63,128,0,0>>, 172 | Message2 = {message,"/1/xy1",[1.0,1.0]}, 173 | Result2 = <<47,49,47,120,121,49,0,0,44,102,102,0,63,128,0,0,63,128,0,0>>, 174 | Bundle = {bundle, {time, 10, 5}, 175 | [{message, "/1/play", [1.0]}, {message, "/1/xy1", [1.0,1.0]}]}, 176 | Result3 = <<35,98,117,110,100,108,101,0,0,0,0,10,0,0,0,5,0,0,0,16,47,49, 177 | 47,112,108,97,121,0,44,102,0,0,63,128,0,0,0,0,0,20,47,49,47, 178 | 120,121,49,0,0,44,102,102,0,63,128,0,0,63,128,0,0>>, 179 | [?_assertEqual(Result1, encode(Message1)), 180 | ?_assertEqual(Message1, decode(Result1)), 181 | ?_assertEqual(Result2, encode(Message2)), 182 | ?_assertEqual(Message2, decode(Result2)), 183 | ?_assertEqual(Result3, encode(Bundle)), 184 | ?_assertEqual(Bundle, decode(Result3))]. 185 | 186 | %% @doc Encodes bundle elements. 187 | %% @spec encode_bundle_elems([message() | bundle()], list()) -> binary() 188 | encode_bundle_elems([], Acc) -> 189 | list_to_binary(lists:reverse(Acc)); 190 | encode_bundle_elems([Element|Rest], Acc) -> 191 | Bin = encode(Element), 192 | Size = size(Bin), 193 | encode_bundle_elems(Rest, [[<>]|Acc]). 194 | 195 | %% @hidden 196 | encode_bundle_elems_test_() -> 197 | Messages = [{message, "/1/play", [1.0]}, {message, "/1/xy1", [1.0,1.0]}], 198 | Result = <<0,0,0,16,47,49,47,112,108,97,121,0,44,102,0,0,63,128,0,0, 199 | 0,0,0,20,47,49,47,120,121,49,0,0,44,102,102,0,63,128,0,0,63,128,0,0>>, 200 | [?_assertEqual(Result, encode_bundle_elems(Messages, [])), 201 | ?_assertEqual(Messages, decode_bundle_elems(Result, []))]. 202 | 203 | %% @doc Encodes times. 204 | %% @spec encode_time(Time::time()) -> binary() 205 | encode_time(immediately) -> 206 | <<1:64>>; 207 | encode_time({time, Seconds, Fractions}) -> 208 | <>. 209 | 210 | %% @hidden 211 | encode_time_test_() -> 212 | [?_assertEqual(<<1:64>>, encode_time(immediately)), 213 | ?_assertEqual(<<10:32,5:32>>, encode_time({time, 10, 5}))]. 214 | 215 | %% @doc Encodes the string by zero-terminating it and padding to 4 chars. 216 | %% @spec encode_string(string()) -> binary() 217 | encode_string(String) when is_list(String) -> 218 | pad(list_to_binary(String ++ [0]), 4). 219 | 220 | %% @hidden 221 | encode_strings_test_() -> 222 | [?_assertEqual(<<"hello",0,0,0>>, encode_string("hello")), 223 | ?_assertEqual(<<"hello1",0,0>>, encode_string("hello1")), 224 | ?_assertEqual(<<"hello12",0>>, encode_string("hello12")), 225 | ?_assertEqual(<<"hello123",0,0,0,0>>, encode_string("hello123"))]. 226 | 227 | %% @doc Encodes the BLOB. 228 | %% @spec encode_blob(binary()) -> Blob::binary() 229 | encode_blob(Bin) when is_binary(Bin) -> 230 | pad(<<(byte_size(Bin)):32, Bin/binary>>, 4). 231 | 232 | %% @hidden 233 | encode_blobs_test_() -> 234 | [?_assertEqual(<<0,0,0,0>>, encode_blob(<<>>)), 235 | ?_assertEqual(<<0,0,0,1,1,0,0,0>>, encode_blob(<<1>>)), 236 | ?_assertEqual(<<0,0,0,2,1,2,0,0>>, encode_blob(<<1,2>>)), 237 | ?_assertEqual(<<0,0,0,3,1,2,3,0>>, encode_blob(<<1,2,3>>)), 238 | ?_assertEqual(<<0,0,0,4,1,2,3,4>>, encode_blob(<<1,2,3,4>>))]. 239 | 240 | %% @doc Checks whether a list is a string. 241 | %% @spec is_string(List) -> true | false 242 | is_string([]) -> true; 243 | is_string([H|_]) when not is_integer(H) -> false; 244 | is_string([_|T]) -> is_string(T); 245 | is_string(_) -> false. 246 | 247 | %% @hidden 248 | is_string_test_() -> 249 | [?_assertEqual(true, is_string("foo")), 250 | ?_assertEqual(false, is_string([one,2]))]. 251 | 252 | %% @doc Encodes args. 253 | %% @spec encode_args(Args::args()) -> Bytes::binary() 254 | encode_args(Args) -> 255 | encode_args(Args, [], []). 256 | 257 | encode_args([], Acc, Types) -> 258 | {list_to_binary(Acc), lists:flatten(Types)}; 259 | encode_args([{i,Int32}|Rest], Acc, Types) -> 260 | encode_args(Rest, [Acc,<>], [Types,$i]); 261 | encode_args([Int32|Rest], Acc, Types) when is_integer(Int32) -> 262 | encode_args([{i,Int32}|Rest], Acc, Types); 263 | encode_args([{f,Float}|Rest], Acc, Types) -> 264 | encode_args(Rest, [Acc,<>], [Types,$f]); 265 | encode_args([Float|Rest], Acc, Types) when is_float(Float) -> 266 | encode_args([{f,Float}|Rest], Acc, Types); 267 | encode_args([{s,String}|Rest], Acc, Types) -> 268 | encode_args(Rest, [Acc,encode_string(String)], [Types,$s]); 269 | encode_args([{b,Blob}|Rest], Acc, Types) -> 270 | encode_args(Rest, [Acc,encode_blob(Blob)], [Types,$b]); 271 | encode_args([{h,Int64}|Rest], Acc, Types) -> 272 | encode_args(Rest, [Acc,<>], [Types,$h]); 273 | encode_args([{time,Seconds,Fractions}|Rest], Acc, Types) -> 274 | encode_args(Rest, [Acc,encode_time({time,Seconds,Fractions})], [Types,$t]); 275 | encode_args([immediately|Rest], Acc, Types) -> 276 | encode_args(Rest, [Acc,encode_time(immediately)], [Types,$t]); 277 | encode_args([{d,Double}|Rest], Acc, Types) -> 278 | encode_args(Rest, [Acc,<>], [Types,$d]); 279 | encode_args([true|Rest], Acc, Types) -> 280 | encode_args(Rest, [Acc,<<>>], [Types,$T]); 281 | encode_args([false|Rest], Acc, Types) -> 282 | encode_args(Rest, [Acc,<<>>], [Types,$F]); 283 | encode_args([null|Rest], Acc, Types) -> 284 | encode_args(Rest, [Acc,<<>>], [Types,$N]); 285 | encode_args([impulse|Rest], Acc, Types) -> 286 | encode_args(Rest, [Acc,<<>>], [Types,$I]); 287 | encode_args([Symbol|Rest], Acc, Types) when is_atom(Symbol) -> 288 | encode_args(Rest, [Acc,encode_string(atom_to_list(Symbol))], [Types,$S]); 289 | encode_args([{c,Char}|Rest], Acc, Types) when is_integer(Char) -> 290 | encode_args(Rest, [Acc,<>], [Types,$c]); 291 | encode_args([{rgba,R,G,B,A}|Rest], Acc, Types) -> 292 | encode_args(Rest, [Acc,<>], 293 | [Types,$r]); 294 | encode_args([{midi,Port,Status,Data1,Data2}|Rest], Acc, Types) -> 295 | encode_args(Rest, [Acc,<>], [Types,$m]); 296 | encode_args([L|Rest], Acc, Types) when is_list(L) -> 297 | case is_string(L) of 298 | true -> 299 | encode_args([{s,L}|Rest], Acc, Types); 300 | false -> 301 | {Bytes, Types2} = encode_args(L, [], []), 302 | encode_args(Rest, [Acc,Bytes], [Types,$[,Types2,$]]) 303 | end. 304 | 305 | %% @hidden 306 | encode_args_test() -> 307 | Bin = <<1:32,2:32,1:64,100,97,116,97,0,0,0,0,2.5:32/float,42:64, 308 | 0,0,0,1,1,0,0,0,102,111,111,0,97:32,255,255,255,255>>, 309 | Types = "i[it[sf]h]bScrTFI", 310 | A = [{i,1},[{i,2},immediately,[{s,"data"},{f,2.5}],{h,42}],{b,<<1>>},foo, 311 | {c,$a},{rgba,255,255,255,255},true,false,impulse], 312 | ?assertEqual({Bin, Types}, encode_args(A)), 313 | B = [1,[2,immediately,["data",2.5],{h,42}],{b,<<1>>},'foo',{c,$a}, 314 | {rgba,255,255,255,255},true,false,impulse], 315 | ?assertEqual({Bin, Types}, encode_args(B)). 316 | 317 | %% @doc Encodes type identifiers 318 | %% @spec encode_types(Types, []) -> binary() 319 | encode_types([], Acc) -> 320 | pad(list_to_binary([<<$,>>,Acc]), 4); 321 | encode_types([Type|Rest], Acc) -> 322 | encode_types(Rest, [Acc,<>]). 323 | 324 | %% @hidden 325 | encode_types_test() -> 326 | ?assertEqual(<<44,102,102,0>>, encode_types("ff", [])). 327 | --------------------------------------------------------------------------------