├── .github └── workflows │ ├── coverage.yml │ ├── docs.yml │ ├── lint.yml │ └── unit-tests.yml ├── .gitignore ├── .ldoc ├── .luacheckrc ├── .luacov ├── LICENSE ├── README.md ├── benchmarks └── init.lua ├── bin ├── loscrecv └── loscsend ├── examples ├── client.lua └── server.lua ├── rockspecs ├── losc-1.0.0-1.rockspec ├── losc-1.0.1-1.rockspec └── losc-scm-0.rockspec ├── spec ├── bundle_spec.lua ├── losc_spec.lua ├── message_spec.lua ├── packet_spec.lua ├── pattern_spec.lua ├── timetag_spec.lua ├── types │ ├── blob_spec.lua │ ├── double_spec.lua │ ├── extended_spec.lua │ ├── float_spec.lua │ ├── int32_spec.lua │ └── string_spec.lua └── types_spec.lua └── src └── losc ├── bundle.lua ├── init.lua ├── lib └── struct.lua ├── message.lua ├── packet.lua ├── pattern.lua ├── plugins ├── udp-libuv.lua └── udp-socket.lua ├── serializer.lua ├── timetag.lua └── types.lua /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | coverage: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@v2 17 | - name: Install Lua/LuaJIT 18 | uses: leafo/gh-actions-lua@v8.0.0 19 | - name: Install luarocks 20 | uses: leafo/gh-actions-luarocks@v4.0.0 21 | - name: Install rocks 22 | run: | 23 | luarocks install busted 24 | luarocks install luacov 25 | luarocks install luacov-reporter-lcov 26 | exit 0 27 | - name: Create coverage 28 | run: | 29 | busted --coverage --output plainTerminal 30 | luacov -r lcov 31 | - name: Coveralls 32 | uses: coverallsapp/github-action@master 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | path-to-lcov: ./luacov.report.out 36 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: leafo/gh-actions-lua@v8.0.0 14 | with: 15 | luaVersion: "5.1.5" 16 | - uses: leafo/gh-actions-luarocks@v4.0.0 17 | - name: build 18 | run: luarocks install ldoc 19 | - name: generate 20 | run: ldoc src -c .ldoc . 21 | - uses: peaceiris/actions-gh-pages@v3 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | publish_dir: ./docs 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out repository 16 | uses: actions/checkout@v2 17 | - name: Install Lua/LuaJIT 18 | uses: leafo/gh-actions-lua@v8.0.0 19 | - name: Install luarocks 20 | uses: leafo/gh-actions-luarocks@v4.0.0 21 | - name: Install luacheck 22 | run: luarocks install luacheck 23 | - name: lint 24 | run: luacheck src 25 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: unit-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | unit-tests: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | lua-version: ['lua 5.1', 'lua 5.2', 'lua 5.3', 'lua 5.4', 'luajit 2.0', 'luajit 2.1.0-beta3'] 17 | steps: 18 | - name: Check out repository 19 | uses: actions/checkout@v2 20 | - uses: actions/cache@v2 21 | id: cache 22 | with: 23 | path: ./.env 24 | key: ${{ runner.os }}-${{ matrix.lua-version }}-${{ secrets.CACHE_VERSION }} 25 | - name: Setup python 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: '3.x' 29 | architecture: 'x64' 30 | - name: Install hererocks 31 | if: steps.cache.outputs.cache-hit != 'true' 32 | run: pip install git+https://github.com/luarocks/hererocks 33 | - name: Install lua 34 | if: steps.cache.outputs.cache-hit != 'true' 35 | run: | 36 | hererocks .env --${{ matrix.lua-version }} --luarocks latest 37 | source .env/bin/activate 38 | luarocks install inspect 39 | luarocks install busted 40 | luarocks install luacov 41 | luarocks install luacov-reporter-lcov 42 | exit 0 43 | - name: Run tests 44 | run: | 45 | source .env/bin/activate 46 | busted --output plainTerminal 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env/ 2 | sketch 3 | docs 4 | luacov.* 5 | -------------------------------------------------------------------------------- /.ldoc: -------------------------------------------------------------------------------- 1 | project = 'losc' 2 | title = 'losc reference' 3 | file = 'src' 4 | dir = 'docs' 5 | examples = 'examples' 6 | format = 'markdown' 7 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | return { 2 | exclude_files = {'src/losc/lib/struct.lua'} 3 | } 4 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | include = { 2 | 'src%/losc$', 3 | 'src%/losc%/.+$', 4 | 'src%/losc%/plugins%/.+$', 5 | } 6 | exclude = { 7 | 'src/losc/lib/.+$', 8 | 'src/losc/serializer', 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Granström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # losc 2 | 3 | [![lint](https://github.com/davidgranstrom/losc/workflows/lint/badge.svg)](https://github.com/davidgranstrom/losc/actions?query=workflow%3Alint) 4 | [![unit-tests](https://github.com/davidgranstrom/losc/workflows/unit-tests/badge.svg)](https://github.com/davidgranstrom/losc/actions?query=workflow%3Aunit-tests) 5 | [![documentation](https://github.com/davidgranstrom/losc/workflows/docs/badge.svg)][docs] 6 | [![Coverage Status](https://coveralls.io/repos/github/davidgranstrom/losc/badge.svg?branch=main)](https://coveralls.io/github/davidgranstrom/losc?branch=main) 7 | 8 | [OSC][osc] 1.0 implementation for `lua` and `luajit`. 9 | 10 | Compatible with lua 5.1 >= 5.4 luajit 2.0 and luajit 2.1.0-beta3 11 | 12 | ## Features 13 | 14 | * Implements the complete OSC 1.0 specification. 15 | * Pure lua implementation, no platform dependent libraries. 16 | * Support for extended OSC types. 17 | * Plugin system for transport layers. 18 | * Address pattern matching. 19 | * Scheduled bundle evaluation (plugin dependent). 20 | 21 | ## Installation 22 | 23 | ``` 24 | luarocks install losc 25 | ``` 26 | 27 | Or clone this repo and copy the `losc` directory into your lua project. 28 | 29 | ## Basic usage 30 | 31 | ```lua 32 | local losc = require'losc' 33 | local plugin = require'losc.plugins.udp-socket' 34 | 35 | local udp = plugin.new {sendAddr = 'localhost', sendPort = 9000} 36 | local osc = losc.new {plugin = udp} 37 | 38 | -- Create a message 39 | local message = osc.new_message { 40 | address = '/foo/bar', 41 | types = 'ifsb', 42 | 123, 1.234, 'hi', 'blobdata' 43 | } 44 | 45 | -- Send it over UDP 46 | osc:send(message) 47 | ``` 48 | 49 | ## Command line utilities 50 | 51 | `losc` provides two command line tools, `loscsend`/`loscrecv` that can be used 52 | to send and receive OSC data via UDP. 53 | 54 | Note that both tools requires [`lua-socket`](https://luarocks.org/modules/luasocket/luasocket). 55 | 56 | ```shell 57 | loscsend - Send an OSC message via UDP. 58 | 59 | usage: loscsend ip port address [types [args]] 60 | supported types: b, d, f, h, i, s, t 61 | example: loscsend localhost 57120 /test ifs 1 2.3 "hi" 62 | ``` 63 | 64 | ```shell 65 | loscrecv - Dump incoming OSC data. 66 | 67 | usage: loscsend port 68 | example: loscrecv 9000 69 | ``` 70 | 71 | ## API 72 | 73 | The API is divided into two parts: 74 | 75 | 1. Opaque high level API exposed through the `losc.lua` module. 76 | 2. Low level API exposing serialization/deserialization functions and types based on plain lua tables. 77 | 78 | The two API:s are decoupled from each other which makes it possible to 79 | implement new high level API:s on top of the low level API functions if needed. 80 | 81 | Read more about it in the [documentation][docs]. 82 | 83 | ## Benchmarks 84 | 85 | Generated with lua 5.1.5 (using `struct`) running on a 2.3 GHz Intel i5 processor. 86 | 87 | ```plain 88 | Message pack: 89 | -> Iterations: 1000 90 | -> Time: 5.765507 ms 91 | -> Avg: 0.005765507 ms 92 | -> Bytes: 48000 93 | Message unpack: 94 | -> Iterations: 1000 95 | -> Time: 5.715137 ms 96 | -> Avg: 0.005715137 ms 97 | -> Bytes: 48000 98 | Bundle pack: 99 | -> Iterations: 1000 100 | -> Time: 15.804163 ms 101 | -> Avg: 0.015804163 ms 102 | -> Bytes: 120000 103 | Bundle unpack: 104 | -> Iterations: 1000 105 | -> Time: 11.864412 ms 106 | -> Avg: 0.011864412 ms 107 | -> Bytes: 120000 108 | ``` 109 | 110 | ### Note on performance 111 | 112 | The OSC serialization functions are implemented differently depending on the 113 | lua interpreter in use and locally available packages. `losc` will always work 114 | out-of-the-box regardless of lua version since 115 | [`lua-struct`](https://github.com/iryont/lua-struct) is a bundled dependency. 116 | 117 | To achieve **better performance** for lua versions < 5.3 it is 118 | recommended to install the [`struct`](http://www.inf.puc-rio.br/~roberto/struct/) package locally. 119 | 120 | ## License 121 | 122 | ``` 123 | MIT License 124 | 125 | Copyright (c) 2021 David Granström 126 | 127 | Permission is hereby granted, free of charge, to any person obtaining a copy 128 | of this software and associated documentation files (the "Software"), to deal 129 | in the Software without restriction, including without limitation the rights 130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 131 | copies of the Software, and to permit persons to whom the Software is 132 | furnished to do so, subject to the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be included in all 135 | copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 138 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 139 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 140 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 141 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 142 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 143 | SOFTWARE. 144 | ``` 145 | 146 | [osc]: http://opensoundcontrol.org/spec-1_0 147 | [docs]: https://davidgranstrom.github.io/losc/ 148 | -------------------------------------------------------------------------------- /benchmarks/init.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path .. ';./src/?.lua' 2 | package.path = package.path .. ';./src/?/?.lua' 3 | 4 | local uv = require'luv' 5 | local Message = require'losc.message' 6 | local Bundle = require'losc.bundle' 7 | local Timetag = require'losc.timetag' 8 | 9 | local function bench(fn, iterations) 10 | -- warm up 11 | for i = 1, 10 do fn() end 12 | local start = uv.hrtime() -- nanoseconds 13 | local bytes = 0 14 | for i = 1, iterations do 15 | bytes = bytes + fn() 16 | end 17 | return (uv.hrtime() - start) / 1000000, bytes 18 | end 19 | 20 | local function report(str, iterations, time, bytes) 21 | print(str .. ':') 22 | print(' -> Iterations: ' .. iterations) 23 | print(' -> Time: ' .. time .. ' ms') 24 | print(' -> Avg: ' .. time / iterations .. ' ms') 25 | print(' -> Bytes: ' .. bytes) 26 | end 27 | 28 | local time, bytes 29 | local iterations = 1000 30 | 31 | -- 48 byte message 32 | local msg = { 33 | address = '/foo/12', -- 8 34 | types = 'ifsb', -- 8 35 | 1, 2.5, 'hello world', 'blobdata' -- 4, 4, 12, 16 36 | } 37 | 38 | time, bytes = bench(function() 39 | return #Message.pack(msg) 40 | end, iterations) 41 | report('Message pack', iterations, time, bytes) 42 | 43 | local data = Message.pack(msg) 44 | 45 | time, bytes = bench(function() 46 | local message, offset = Message.unpack(data) 47 | return offset - 1 48 | end, iterations) 49 | report('Message unpack', iterations, time, bytes) 50 | 51 | local sec, usec = uv.gettimeofday() 52 | local tt = Timetag.new(sec, usec, 1e6) 53 | local bndl = { 54 | timetag = tt.content, 55 | msg, 56 | msg, 57 | } 58 | 59 | time, bytes = bench(function() 60 | return #Bundle.pack(bndl) 61 | end, iterations) 62 | report('Bundle pack', iterations, time, bytes) 63 | 64 | data = Bundle.pack(bndl) 65 | 66 | time, bytes = bench(function() 67 | local bundle, offset = Bundle.unpack(data) 68 | return offset - 1 69 | end, iterations) 70 | report('Bundle unpack', iterations, time, bytes) 71 | -------------------------------------------------------------------------------- /bin/loscrecv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local losc = require'losc' 4 | local Timetag = require'losc.timetag' 5 | local ok, plugin = pcall(require, 'losc.plugins.udp-socket') 6 | if not ok then 7 | local msg = 'loscsend requires `luasocket`\nTry: `luarocks install luasocket`' 8 | io.stderr:write(msg) 9 | return 10 | end 11 | 12 | local function usage() 13 | local str = '' 14 | str = str .. 'loscrecv - Dump incoming OSC data.\n' 15 | str = str .. '\nusage: loscsend port' 16 | str = str .. '\nexample: loscrecv 9000\n' 17 | io.write(str) 18 | end 19 | 20 | -- Main 21 | 22 | if arg and arg[1] == '-h' or arg[1] == '--help' then 23 | usage() 24 | os.exit(0) 25 | end 26 | 27 | local port = arg[1] 28 | if not port then 29 | usage() 30 | os.exit(0) 31 | end 32 | 33 | local osc = losc.new {plugin = plugin.new()} 34 | 35 | osc:add_handler('*', function(data) 36 | local tt = Timetag.new_from_timestamp(data.timestamp) 37 | local time = string.format('%08x.%08x', tt:seconds(), tt:fractions()) 38 | local ok, message = pcall(osc.new_message, data.message) 39 | if not ok then 40 | print(message) 41 | return 42 | end 43 | io.write(time .. ' ') 44 | io.write(message:address() .. ' ') 45 | io.write(message:types() .. ' ') 46 | for i, type_, arg_ in message:iter() do 47 | if type_ == 's' then 48 | io.write(string.format('"%s" ', arg_)) 49 | elseif type_ == 'b' then 50 | io.write(string.format('[%d byte blob] ', #arg_)) 51 | elseif type_ == 'f' or type_ == 'd' then 52 | io.write(string.format('%06f ', arg_)) 53 | elseif type_ == 'i' or type_ 'h' then 54 | io.write(string.format('%d ', arg_)) 55 | elseif type_ == 't' then 56 | local tmp = Timetag.new_from_timestamp(arg_) 57 | string.format('%08x.%08x', tmp:seconds(), tmp:fractions()) 58 | else 59 | io.write(tostring(arg_) .. ' ') 60 | end 61 | end 62 | io.write('\n') 63 | end) 64 | osc:open('127.0.0.1', arg[1] or 0) 65 | -------------------------------------------------------------------------------- /bin/loscsend: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local losc = require'losc' 4 | local Types = require'losc.types' 5 | local ok, plugin = pcall(require, 'losc.plugins.udp-socket') 6 | if not ok then 7 | local msg = 'loscsend requires `luasocket`\nTry: `luarocks install luasocket`' 8 | io.stderr:write(msg) 9 | return 10 | end 11 | 12 | local unpack = unpack or table.unpack 13 | local osc = losc.new {plugin = plugin.new()} 14 | 15 | local function usage() 16 | local str = '' 17 | str = str .. 'loscsend - Send an OSC message via UDP.\n' 18 | str = str .. '\nusage: loscsend ip port address [types [args]]' 19 | str = str .. '\nsupported types: ' 20 | local types = Types.get(Types.pack) 21 | table.sort(types) 22 | str = str .. table.concat(types, ', ') 23 | str = str .. '\n\nexample: loscsend localhost 57120 /test ifs 1 2.3 "hi"\n' 24 | io.write(str) 25 | end 26 | 27 | local function opt_parse(options) 28 | local ip, port, address, types = unpack(options) 29 | if ip == '-h' or ip == '--help' then 30 | usage() 31 | os.exit(0) 32 | end 33 | return ip, tonumber(port), address, types 34 | end 35 | 36 | -- Main 37 | 38 | if not arg[1] then 39 | usage() 40 | os.exit(0) 41 | end 42 | 43 | local ip, port, address, types = opt_parse(arg) 44 | local ok, message = pcall(losc.new_message, address) 45 | if not ok then 46 | print(message) 47 | os.exit(1) 48 | end 49 | 50 | if types then 51 | local index = 5 52 | for type in types:gmatch('.') do 53 | local item = arg[index] 54 | if string.match(type, '[ifdht]') then 55 | item = tonumber(item) 56 | end 57 | message:add(type, item) 58 | index = index + 1 59 | end 60 | end 61 | 62 | if ip and port then 63 | osc:send(message, ip, port) 64 | else 65 | print('Must specify ip and port. See loscsend -h for usage.') 66 | os.exit(1) 67 | end 68 | 69 | osc:close() 70 | -------------------------------------------------------------------------------- /examples/client.lua: -------------------------------------------------------------------------------- 1 | ----------------- 2 | -- Simple client. 3 | -- Uses the udp-socket plugin. 4 | 5 | local losc = require'losc' 6 | local plugin = require'losc.plugins.udp-socket' 7 | 8 | local udp = plugin.new {sendAddr = 'localhost', sendPort = 9000} 9 | local osc = losc.new {plugin = udp} 10 | 11 | local message = losc.new_message { 12 | address = '/foo/bar', 13 | types = 'ifsb', 14 | 123, 1.234, 'hi', 'blobdata' 15 | } 16 | 17 | osc:send(message) 18 | -------------------------------------------------------------------------------- /examples/server.lua: -------------------------------------------------------------------------------- 1 | ----------------- 2 | -- Simple server. 3 | -- Uses the udp-socket plugin. 4 | 5 | local losc = require'losc' 6 | local plugin = require'losc.plugins.udp-socket' 7 | 8 | local udp = plugin.new { 9 | recvAddr = 'localhost', 10 | recvPort = 9000, 11 | ignore_late = true, -- ignore late bundles 12 | } 13 | local osc = losc.new {plugin = udp} 14 | 15 | local function print_data(data) 16 | local msg = data.message 17 | print('address: ' .. msg.address, 'timestamp: ' .. data.timestamp) 18 | for index, argument in ipairs(msg) do 19 | print('index: ' .. index, 'arg: ' .. argument) 20 | end 21 | end 22 | 23 | osc:add_handler('/test', function(data) 24 | print_data(data) 25 | end) 26 | 27 | osc:add_handler('/param/{x,y,z}', function(data) 28 | print_data(data) 29 | end) 30 | 31 | osc:open() -- blocking call (depending on plugin used) 32 | -------------------------------------------------------------------------------- /rockspecs/losc-1.0.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "losc" 2 | version = "1.0.0-1" 3 | source = { 4 | url = "git+https://github.com/davidgranstrom/losc.git", 5 | tag = "v1.0.0", 6 | } 7 | description = { 8 | summary = "Open Sound Control (OSC) library.", 9 | detailed = [[ 10 | Open Sound Control (OSC) for lua/luajit with no external dependencies. 11 | ]], 12 | homepage = "https://github.com/davidgranstrom/losc", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["losc.bundle"] = "src/losc/bundle.lua", 22 | ["losc.init"] = "src/losc/init.lua", 23 | ["losc.lib.struct"] = "src/losc/lib/struct.lua", 24 | ["losc.message"] = "src/losc/message.lua", 25 | ["losc.packet"] = "src/losc/packet.lua", 26 | ["losc.pattern"] = "src/losc/pattern.lua", 27 | ["losc.plugins.udp-libuv"] = "src/losc/plugins/udp-libuv.lua", 28 | ["losc.plugins.udp-socket"] = "src/losc/plugins/udp-socket.lua", 29 | ["losc.serializer"] = "src/losc/serializer.lua", 30 | ["losc.timetag"] = "src/losc/timetag.lua", 31 | ["losc.types"] = "src/losc/types.lua" 32 | }, 33 | install = { 34 | bin = { 35 | "bin/loscrecv", 36 | "bin/loscsend" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rockspecs/losc-1.0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "losc" 2 | version = "1.0.1-1" 3 | source = { 4 | url = "git+https://github.com/davidgranstrom/losc.git", 5 | tag = "v1.0.1", 6 | } 7 | description = { 8 | summary = "Open Sound Control (OSC) library.", 9 | detailed = [[ 10 | Open Sound Control (OSC) for lua/luajit with no external dependencies. 11 | ]], 12 | homepage = "https://github.com/davidgranstrom/losc", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua >= 5.1", 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["losc.bundle"] = "src/losc/bundle.lua", 22 | ["losc.init"] = "src/losc/init.lua", 23 | ["losc.lib.struct"] = "src/losc/lib/struct.lua", 24 | ["losc.message"] = "src/losc/message.lua", 25 | ["losc.packet"] = "src/losc/packet.lua", 26 | ["losc.pattern"] = "src/losc/pattern.lua", 27 | ["losc.plugins.udp-libuv"] = "src/losc/plugins/udp-libuv.lua", 28 | ["losc.plugins.udp-socket"] = "src/losc/plugins/udp-socket.lua", 29 | ["losc.serializer"] = "src/losc/serializer.lua", 30 | ["losc.timetag"] = "src/losc/timetag.lua", 31 | ["losc.types"] = "src/losc/types.lua" 32 | }, 33 | install = { 34 | bin = { 35 | "bin/loscrecv", 36 | "bin/loscsend" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rockspecs/losc-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "losc" 2 | version = "scm-0" 3 | source = { 4 | url = "git+https://github.com/davidgranstrom/losc.git" 5 | } 6 | description = { 7 | summary = "OSC 1.0 library.", 8 | detailed = [[ 9 | Open Sound Control (OSC) for lua/luajit with no external dependencies. 10 | ]], 11 | homepage = "https://github.com/davidgranstrom/losc", 12 | license = "MIT" 13 | } 14 | dependencies = { 15 | "lua >= 5.1", 16 | } 17 | build = { 18 | type = "builtin", 19 | modules = { 20 | ["losc.bundle"] = "src/losc/bundle.lua", 21 | ["losc.init"] = "src/losc/init.lua", 22 | ["losc.lib.struct"] = "src/losc/lib/struct.lua", 23 | ["losc.message"] = "src/losc/message.lua", 24 | ["losc.packet"] = "src/losc/packet.lua", 25 | ["losc.pattern"] = "src/losc/pattern.lua", 26 | ["losc.plugins.udp-libuv"] = "src/losc/plugins/udp-libuv.lua", 27 | ["losc.plugins.udp-socket"] = "src/losc/plugins/udp-socket.lua", 28 | ["losc.serializer"] = "src/losc/serializer.lua", 29 | ["losc.timetag"] = "src/losc/timetag.lua", 30 | ["losc.types"] = "src/losc/types.lua" 31 | }, 32 | install = { 33 | bin = { 34 | "bin/loscrecv", 35 | "bin/loscsend" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /spec/bundle_spec.lua: -------------------------------------------------------------------------------- 1 | local Message = require'losc.message' 2 | local Bundle = require'losc.bundle' 3 | local Timetag = require'losc.timetag' 4 | 5 | describe('Bundle', function() 6 | describe('constructors', function() 7 | it('can create a new bundle', function() 8 | local tt = Timetag.new(os.time()) 9 | local bndl = Bundle.new(tt) 10 | assert.not_nil(bndl) 11 | assert.is_true(pcall(Bundle.tbl_validate, bndl.content)) 12 | end) 13 | 14 | it('can add to an existing bundle', function() 15 | local tt = Timetag.new(os.time()) 16 | local bndl = Bundle.new(tt) 17 | local msg = Message.new('/fo') 18 | msg:add('i', 1) 19 | bndl:add(msg) 20 | assert.are.equal(1, #bndl.content) 21 | end) 22 | end) 23 | 24 | describe('pack', function() 25 | it('requires a timetag', function() 26 | local bndl = { 27 | {address = '/foo', types = 'iii', 1, 2, 3} 28 | } 29 | assert.has_errors(function() 30 | Bundle.pack(bndl) 31 | end) 32 | end) 33 | 34 | it('can pack messages correctly', function() 35 | local bndl = { 36 | timetag = {seconds = 0, fractions = 1}, 37 | {address = 'hello', types = 'T'}, 38 | {address = 'world', types = 'i', 1}, 39 | } 40 | local data = Bundle.pack(bndl); 41 | assert.not_nil(data) 42 | -- (8 + 8) + (4 + 12) + (4 + 16) 43 | assert.are.equal(52, #data) 44 | end) 45 | 46 | it('handles bundle with no contents', function() 47 | local bndl = { timetag = {seconds = 0, fractions = 1} } 48 | local data = Bundle.pack(bndl); 49 | assert.not_nil(data) 50 | assert.are.equal(#data % 4, 0) 51 | end) 52 | 53 | it('has a size that is an multiple of 4', function() 54 | local bndl = { 55 | timetag = {seconds = 0, fractions = 1}, 56 | {address = 'hello', types = 'T'}, 57 | {address = 'world', types = 'is', 1, 'foo'}, 58 | } 59 | local data = Bundle.pack(bndl); 60 | assert.not_nil(data) 61 | assert.are.equal(#data % 4, 0) 62 | end) 63 | 64 | it('can pack nested bundles', function() 65 | local bndl = { 66 | timetag = {seconds = 0, fractions = 0}, 67 | {address = '/foo', types = 'iii', 1, 2, 3}, 68 | {address = '/bar', types = 'f', 1}, 69 | { 70 | timetag = {seconds = 1, fractions = 0}, 71 | {address = '/baz', types = 'i', 7}, 72 | { 73 | timetag = {seconds = 3, fractions = 0}, 74 | {address = '/abc', types = 'i', 74}, 75 | } 76 | } 77 | } 78 | local data = Bundle.pack(bndl); 79 | assert.not_nil(data) 80 | assert.are.equal(#data % 4, 0) 81 | end) 82 | 83 | it('nested bundle timetag must be >= parent timetag', function() 84 | local now = os.time() 85 | local tt = Timetag.new(now) 86 | local tt2 = tt + 1 87 | local bndl = { 88 | timetag = tt2.content, 89 | {address = '/foo', types = 'iii', 1, 2, 3}, 90 | { 91 | timetag = tt.content, 92 | } 93 | } 94 | assert.has_errors(function() 95 | Bundle.pack(bndl) 96 | end) 97 | end) 98 | end) 99 | 100 | describe('unpack', function() 101 | local now = os.time() 102 | local tt = Timetag.new(now) 103 | local tt1 = tt + 1 104 | local tt2 = tt + 2 105 | local bundle 106 | local data = { 107 | timetag = tt.content, 108 | {address = '/xxx', types = 'iii', 1, 2, 3}, 109 | { 110 | timetag = tt1.content, 111 | {address = '/baz', types = 'i', 7}, 112 | {address = '/yyy', types = 'i', 7}, 113 | { 114 | timetag = tt2.content, 115 | {address = '/abc', types = 'i', 1}, 116 | {address = '/123', types = 'i', 456}, 117 | {address = '/zzz', types = 'i', 999}, 118 | }, 119 | } 120 | } 121 | 122 | setup(function() 123 | bundle = Bundle.unpack(Bundle.pack(data)) 124 | end) 125 | 126 | it('returns a table', function() 127 | assert.are.equal(type(bundle), 'table') 128 | end) 129 | 130 | it('unpacks empty bundle', function() 131 | local bndl = {timetag = {seconds = 0, fractions = 1}} 132 | local res = Bundle.unpack(Bundle.pack(bndl)) 133 | assert.not_nil(res) 134 | assert.are.equal(0, res.timetag.seconds) 135 | assert.are.equal(1, res.timetag.fractions) 136 | assert.are.equal(0, #res) 137 | end) 138 | 139 | local function compare_msg(msg1, msg2) 140 | local equal = msg1.address == msg2.address 141 | equal = equal and msg1.types == msg2.types 142 | for i, v in ipairs(msg1) do 143 | equal = equal and v == msg2[i] 144 | end 145 | return equal 146 | end 147 | 148 | it('unpacks bundle with messages', function() 149 | local bndl = { 150 | timetag = {seconds = 0, fractions = 1}, 151 | {address = '/foo/bar', types = 'iii', 1, 2, 3}, 152 | {address = '/foo/baz', types = 'iss', 1, 'hello', '#bundle'}, 153 | {address = '/foo/baz', types = 'i', 4}, 154 | } 155 | local res = Bundle.unpack(Bundle.pack(bndl)) 156 | assert.not_nil(res) 157 | assert.are.equal(bndl.timetag.seconds, res.timetag.seconds) 158 | assert.are.equal(bndl.timetag.fractions, res.timetag.fractions) 159 | assert.are.equal(#bndl, #res) 160 | for i, msg in ipairs(res) do 161 | assert.is_true(compare_msg(bndl[i], msg)) 162 | end 163 | end) 164 | 165 | it('unpacks nested bundles', function() 166 | assert.are.equal(data.timetag.seconds, bundle.timetag.seconds) 167 | assert.are.equal(data.timetag.fractions, bundle.timetag.fractions) 168 | assert.are.equal(data[2].timetag.seconds, bundle[2].timetag.seconds) 169 | assert.are.equal(data[2].timetag.fractions, bundle[2].timetag.fractions) 170 | assert.are.equal(data[2][3].timetag.seconds, bundle[2][3].timetag.seconds) 171 | assert.are.equal(data[2][3].timetag.fractions, bundle[2][3].timetag.fractions) 172 | assert.is_true((compare_msg(data[1], bundle[1]))) 173 | assert.is_true((compare_msg(data[2][1], bundle[2][1]))) 174 | assert.is_true((compare_msg(data[2][2], bundle[2][2]))) 175 | assert.is_true((compare_msg(data[2][3][1], bundle[2][3][1]))) 176 | assert.is_true((compare_msg(data[2][3][2], bundle[2][3][2]))) 177 | assert.is_true((compare_msg(data[2][3][3], bundle[2][3][3]))) 178 | end) 179 | end) 180 | 181 | describe('methods', function() 182 | local bundle 183 | 184 | setup(function() 185 | local tt = Timetag.new() 186 | bundle = Bundle.new(tt) 187 | end) 188 | 189 | it('can get and set a Timetag', function() 190 | local tt = Timetag.new(123) 191 | local stamp = tt:timestamp(1) 192 | assert.is_true(type(bundle:timetag()) == 'table') 193 | bundle:timetag(tt) -- update 194 | assert.are.equal(stamp, bundle:timetag():timestamp(1)) 195 | end) 196 | end) 197 | end) 198 | -------------------------------------------------------------------------------- /spec/losc_spec.lua: -------------------------------------------------------------------------------- 1 | local losc = require'losc' 2 | local Timetag = require'losc.timetag' 3 | local Packet = require'losc.packet' 4 | 5 | local osc = losc.new() 6 | 7 | describe('osc', function() 8 | it('can create a message', function() 9 | local message = osc.new_message('/test') 10 | assert.is_true(pcall(Packet.validate, message)) 11 | message = osc.new_message({address = '/test/123', types = 'if', 1, 2.3}) 12 | assert.is_true(pcall(Packet.validate, message)) 13 | assert.has_errors(function() 14 | osc.new_message({'/test/123'}) 15 | end) 16 | end) 17 | 18 | it('prepends / to message address if missing', function() 19 | local message = osc.new_message('addr') 20 | assert.are.equal('/addr', message:address()) 21 | end) 22 | 23 | it('can create a bundle', function() 24 | local tt = osc:now() 25 | local message = osc.new_message({address = '/test/123', types = 'if', 1, 2.3}) 26 | local bundle = osc.new_bundle(tt, message) 27 | assert.is_true(pcall(Packet.validate, bundle)) 28 | assert.has_errors(function() 29 | osc.new_bundle(nil, message) 30 | end) 31 | end) 32 | 33 | it('can add and remove OSC handlers', function() 34 | local pattern = '/foo/123' 35 | osc:add_handler(pattern, function(data) end) 36 | local pattern2 = '/foo/{bar,baz}/123' 37 | osc:add_handler(pattern2, function(data) end) 38 | assert.not_nil(osc.handlers[pattern]) 39 | assert.not_nil(osc.handlers[pattern2]) 40 | osc:remove_handler(pattern2, function(data) end) 41 | assert.is_nil(osc.handlers[pattern2]) 42 | osc:remove_all() 43 | assert.is_nil(osc.handlers[pattern]) 44 | end) 45 | 46 | it('can use', function() 47 | assert.not_nil(osc.use) 48 | end) 49 | 50 | it('can open', function() 51 | assert.has_errors(function() 52 | osc:open() 53 | end) 54 | end) 55 | 56 | it('can close', function() 57 | assert.has_errors(function() 58 | osc:close() 59 | end) 60 | end) 61 | 62 | it('can send', function() 63 | assert.has_errors(function() 64 | osc:send() 65 | end) 66 | end) 67 | 68 | describe('osc with plugin', function() 69 | setup(function() 70 | -- mock plugin 71 | local plugin = {} 72 | -- timetag precision 73 | plugin.precision = 1000 74 | plugin.now = function() 75 | return Timetag.new(os.time()) 76 | end 77 | plugin.schedule = function(self, timestamp, handler) 78 | handler() 79 | end 80 | plugin.open = function(self) 81 | return {} 82 | end 83 | plugin.close = function(self) 84 | end 85 | plugin.send = function(self, msg) 86 | assert.are.equal(msg.content.address, '/test') 87 | end 88 | osc = losc.new {plugin = plugin} 89 | end) 90 | 91 | it('return current time as Timetag', function() 92 | local now = osc:now() 93 | assert.not_nil(now) 94 | assert.not_nil(now.seconds) 95 | assert.not_nil(now.fractions) 96 | end) 97 | 98 | it('can open', function() 99 | local ok, handle = osc:open() 100 | assert.is_true(ok) 101 | assert.not_nil(handle) 102 | end) 103 | 104 | it('can close', function() 105 | local ok, err = osc:close() 106 | assert.is_true(ok) 107 | end) 108 | 109 | it('can send', function() 110 | local message = osc.new_message('/test') 111 | local ok, err = osc:send(message) 112 | assert.is_true(ok) 113 | end) 114 | end) 115 | end) 116 | -------------------------------------------------------------------------------- /spec/message_spec.lua: -------------------------------------------------------------------------------- 1 | local Message = require'losc.message' 2 | 3 | describe('Message', function() 4 | describe('constructors', function() 5 | it('can create an empty message object', function() 6 | local message = Message.new() 7 | assert.not_nil(message) 8 | assert.is_true(type(message.content) == 'table') 9 | end) 10 | 11 | it('can create message object from table', function() 12 | local msg = {address = '/foo/bar', types = 's', 'hello'} 13 | local message = Message.new(msg) 14 | assert.not_nil(message) 15 | end) 16 | end) 17 | 18 | describe('methods', function() 19 | it('can get the address', function() 20 | local message = Message.new('/foo/bar') 21 | assert.are.equal(message:address(), '/foo/bar') 22 | end) 23 | 24 | it('can get the arguments', function() 25 | local message = Message.new({address = '/foo/bar', types = 'isf', 1, 'foo', 1.234}) 26 | local args = message:args() 27 | assert.are.equal(1, args[1]) 28 | assert.are.equal('foo', args[2]) 29 | assert.are.equal(1.234, args[3]) 30 | end) 31 | 32 | it('can add arguments', function() 33 | local message = Message.new('/foo/bar') 34 | message:add('i', 123) 35 | message:add('f', 1.234) 36 | message:add('T') 37 | message:add('s', 'foo') 38 | message:add('N') 39 | message:add('I') 40 | assert.are.equal('ifTsNI', message:types()) 41 | assert.are.equal(#message.content.types, #message.content) 42 | end) 43 | 44 | it('can iterate over types and arguments', function() 45 | local msg = {address = '/foo/bar', types = 'isFf', 1, 'hello', true, 1.234} 46 | local message = Message.new(msg) 47 | for i, type, arg in message:iter() do 48 | assert.are.equal(msg.types:sub(i, i), type) 49 | assert.are.equal(msg[i], arg) 50 | end 51 | message = Message.new() 52 | for i, type, arg in message:iter() do 53 | assert.is_nil(true) -- this should never be triggered 54 | end 55 | end) 56 | 57 | it('can validate an address', function() 58 | assert.is_false(pcall(Message.address_validate, '/foo/ /123')) 59 | assert.is_false(pcall(Message.address_validate, '/foo/#/123')) 60 | assert.is_false(pcall(Message.address_validate, '/foo/*/123')) 61 | assert.is_false(pcall(Message.address_validate, '/foo/,/123')) 62 | assert.is_false(pcall(Message.address_validate, '/foo/?/123')) 63 | assert.is_false(pcall(Message.address_validate, '/foo/[/123')) 64 | assert.is_false(pcall(Message.address_validate, '/foo/]/123')) 65 | assert.is_false(pcall(Message.address_validate, '/foo/{/123')) 66 | assert.is_false(pcall(Message.address_validate, '/foo/}/123')) 67 | assert.is_true(pcall(Message.address_validate, '/foo/bar/123')) 68 | end) 69 | end) 70 | 71 | describe('pack', function() 72 | it('requires an address', function() 73 | local m = {types = 'i', 1} 74 | assert.has_errors(function() 75 | Message.pack(m) 76 | end) 77 | end) 78 | 79 | it('has a size that is an multiple of 4', function() 80 | local m = { 81 | address = '/fo', 82 | types = 'is', 83 | 123, 84 | 'hello' 85 | } 86 | local buffer = Message.pack(m) 87 | assert.not_nil(buffer) 88 | assert.are.equal(#buffer % 4, 0) 89 | end) 90 | 91 | it('skips types that should not be in argument data', function() 92 | local m = { 93 | address = '/fo', 94 | types = 'TiiFs', 95 | true, 96 | 1, 97 | 2, 98 | false, 99 | 'hi' 100 | } 101 | local data = Message.pack(m) 102 | assert.not_nil(data) 103 | assert.are.equal(24, #data) 104 | assert.are.equal('/fo\0,TiiFs\0\0\0\0\0\1\0\0\0\2hi\0\0', data) 105 | end) 106 | 107 | it('correctly skips false items', function() 108 | local msg = {address = '/', types = 'iFi', 1, false, 2}; 109 | local data = Message.pack(msg) 110 | assert.not_nil(data) 111 | assert.are.equal(20, #data) 112 | assert.are.equal('/\0\0\0,iFi\0\0\0\0\0\0\0\1\0\0\0\2', data) 113 | end) 114 | end) 115 | 116 | describe('unpack', function() 117 | local message 118 | local input = { 119 | address = '/fo', 120 | types = 'isTf', 121 | 123, 122 | 'hello', 123 | true, 124 | 1.234, 125 | } 126 | 127 | setup(function() 128 | local buffer = Message.pack(input) 129 | message = Message.unpack(buffer) 130 | end) 131 | 132 | it('returns a table', function() 133 | assert.are.equal(type(message), 'table') 134 | end) 135 | 136 | it('handles types not represented in OSC data', function() 137 | assert.is_true(message[3]) 138 | end) 139 | 140 | it('unpacks correct values', function() 141 | assert.are.equal(input.address, message.address) 142 | assert.are.equal(input.types, message.types) 143 | assert.are.equal(input[1], message[1]) 144 | assert.are.equal(input[2], message[2]) 145 | assert.are.equal(input[3], message[3]) 146 | assert.is_true(math.abs(input[4] - message[4]) < 1e-4) 147 | end) 148 | end) 149 | end) 150 | -------------------------------------------------------------------------------- /spec/packet_spec.lua: -------------------------------------------------------------------------------- 1 | local losc = require'losc' 2 | local Packet = require'losc.packet' 3 | 4 | describe('Packet', function() 5 | local message, bundle 6 | local mdata, bdata 7 | setup(function() 8 | message = losc.new_message('/foo') 9 | bundle = losc.new_bundle(losc:now(), message) 10 | end) 11 | 12 | it('can pack messages', function() 13 | mdata = Packet.pack(message) 14 | assert.not_nil(mdata) 15 | assert.is.equal(0, #mdata % 4) 16 | end) 17 | 18 | it('can pack bundles', function() 19 | bdata = Packet.pack(bundle) 20 | assert.not_nil(bdata) 21 | assert.is.equal(0, #bdata % 4) 22 | end) 23 | 24 | it('can unpack messages', function() 25 | local m = Packet.unpack(mdata) 26 | assert.not_nil(m) 27 | assert.is.equal(type(m), 'table') 28 | assert.are.equal('/foo', m.address) 29 | end) 30 | 31 | it('can unpack bundles', function() 32 | local b = Packet.unpack(bdata) 33 | assert.not_nil(bdata) 34 | assert.is.equal(type(b), 'table') 35 | assert.are.equal('/foo', b[1].address) 36 | end) 37 | end) 38 | -------------------------------------------------------------------------------- /spec/pattern_spec.lua: -------------------------------------------------------------------------------- 1 | local Pattern = require'losc.pattern' 2 | local Message = require'losc.message' 3 | local Bundle = require'losc.bundle' 4 | local Packet = require'losc.packet' 5 | local Timetag = require'losc.timetag' 6 | local losc = require'losc' 7 | 8 | -- mock plugin 9 | local plugin = {} 10 | -- timetag precision 11 | plugin.precision = 1000 12 | plugin.options = {} 13 | plugin.options.ignore_late = true 14 | plugin.remote_info = {} 15 | plugin.now = function() 16 | return Timetag.new(os.time()) 17 | end 18 | plugin.schedule = function(self, timestamp, handler) 19 | handler() 20 | end 21 | 22 | local osc = losc.new {plugin = plugin} 23 | 24 | before_each(function() 25 | osc:remove_all() 26 | end) 27 | 28 | describe('Pattern', function() 29 | it('can dispatch incoming data', function() 30 | local data = Message.pack({address = '/foo/bar', types = 'i', 1}) 31 | osc:add_handler('/foo/bar', function(data) 32 | assert.not_nil(data) 33 | assert.not_nil(data.message) 34 | assert.not_nil(data.timestamp) 35 | assert.not_nil(data.remote_info) 36 | assert.are.equal('/foo/bar', data.message.address) 37 | assert.are.equal('i', data.message.types) 38 | assert.are.equal(1, data.message[1]) 39 | end) 40 | Pattern.dispatch(data, plugin) 41 | end) 42 | 43 | it('ignores late messages', function() 44 | local num = 0 45 | local message = Message.new {address = '/foo/bar', types = 'i', 1} 46 | local bundle = Bundle.new(Timetag.new(1), message) 47 | osc:add_handler('/foo/bar', function(data) 48 | num = num + 1 49 | end) 50 | Pattern.dispatch(Packet.pack(bundle), plugin) 51 | assert.are.equal(0, num) 52 | plugin.options.ignore_late = false 53 | Pattern.dispatch(Packet.pack(bundle), plugin) 54 | assert.are.equal(1, num) 55 | end) 56 | 57 | it('can dispatch nested bundles', function() 58 | local message = Message.new {address = '/foo/123', types = 'i', 1} 59 | local message2 = Message.new {address = '/foo/abc', types = 'f', 1.234} 60 | local bundle = Bundle.new(Timetag.new(123), message) 61 | local bundle2 = Bundle.new(Timetag.new(), bundle, message2) 62 | local num = 0 63 | osc:add_handler('/foo/*', function(data) 64 | num = num + 1 65 | end) 66 | plugin.options.ignore_late = false 67 | Pattern.dispatch(Packet.pack(bundle2), plugin) 68 | assert.are.equal(2, num) 69 | end) 70 | 71 | it('throws an error if bundled timetag is older than enclosing bundle', function() 72 | local data = '#bundle\0\1\1\1\1\1\1\1\1\0\0\0(#bundle\0\0\0\0\0\0\0\0\1\0\0\0\20/foo/123\0\0\0\0,i\0\0\0\0\0\1\0\0\0\20/foo/abc\0\0\0\0,i\0\0\0\0\0\2' 73 | assert.has_errors(function() 74 | Pattern.dispatch(data, plugin) 75 | end) 76 | end) 77 | 78 | describe('pattern matching', function() 79 | it('can match any single character (?)', function() 80 | local num_matches = 0 81 | osc:add_handler('/foo/ba?', function(data) 82 | assert.not_nil(data) 83 | assert.not_nil(data.message) 84 | assert.not_nil(data.timestamp) 85 | assert.not_nil(data.remote_info) 86 | num_matches = num_matches + 1 87 | end) 88 | local data = Message.pack({address = '/foo/bar', types = 'i', 1}) 89 | Pattern.dispatch(data, plugin) 90 | data = Message.pack({address = '/foo/baz', types = 'i', 1}) 91 | Pattern.dispatch(data, plugin) 92 | assert.are.equal(2, num_matches) 93 | end) 94 | 95 | it('can match any sequence (*)', function() 96 | local num_matches = 0 97 | local data = Message.pack({address = '/foo/bar/baz', types = 'i', 1}) 98 | osc:add_handler('*', function(data) 99 | assert.not_nil(data) 100 | assert.not_nil(data.message) 101 | assert.not_nil(data.timestamp) 102 | assert.not_nil(data.remote_info) 103 | num_matches = num_matches + 1 104 | end) 105 | Pattern.dispatch(data, plugin) 106 | assert.are.equal(1, num_matches) 107 | end) 108 | 109 | it('can match wildcard sequence (*)', function() 110 | local num_matches = 0 111 | osc:add_handler('/foo/*/baz', function(data) 112 | assert.not_nil(data) 113 | num_matches = num_matches + 1 114 | end) 115 | osc:add_handler('/foo/bar/*', function(data) 116 | assert.not_nil(data) 117 | num_matches = num_matches + 1 118 | end) 119 | local shouldmatch = Message.pack({address = '/foo/bar/baz', types = 'i', 1}) 120 | local nomatch = Message.pack({address = '/foo', types = 'i', 1}) 121 | Pattern.dispatch(nomatch, plugin) 122 | assert.are.equal(0, num_matches) 123 | Pattern.dispatch(shouldmatch, plugin) 124 | assert.are.equal(2, num_matches) 125 | end) 126 | 127 | it('can match sequence of characters ([])', function() 128 | local num_matches = 0 129 | osc:add_handler('/foo/[0-9]', function(data) 130 | assert.not_nil(data) 131 | num_matches = num_matches + 1 132 | end) 133 | 134 | local shouldmatch = Message.pack({address = '/foo/1', types = 'i', 1}) 135 | Pattern.dispatch(shouldmatch, plugin) 136 | assert.are.equal(1, num_matches) 137 | shouldmatch = Message.pack({address = '/foo/123', types = 'i', 1}) 138 | Pattern.dispatch(shouldmatch, plugin) 139 | assert.are.equal(2, num_matches) 140 | 141 | osc:remove_all() 142 | 143 | num_matches = 0 144 | osc:add_handler('/[!a-f]/foo', function(data) 145 | assert.not_nil(data) 146 | num_matches = num_matches + 1 147 | end) 148 | local shouldmatch = Message.pack({address = '/ghi/foo', types = 'i', 1}) 149 | local nomatch = Message.pack({address = '/abc/foo', types = 'i', 1}) 150 | Pattern.dispatch(nomatch, plugin) 151 | assert.are.equal(0, num_matches) 152 | Pattern.dispatch(shouldmatch, plugin) 153 | assert.are.equal(1, num_matches) 154 | end) 155 | 156 | it('can match groups {}', function() 157 | local num_matches = 0 158 | osc:add_handler('/foo/{bar,baz}/123', function(data) 159 | assert.not_nil(data) 160 | num_matches = num_matches + 1 161 | end) 162 | local shouldmatch = Message.pack({address = '/foo/bar/123', types = 'i', 1}) 163 | local nomatch = Message.pack({address = '/foo/zig/123', types = 'i', 1}) 164 | Pattern.dispatch(nomatch, plugin) 165 | assert.are.equal(0, num_matches) 166 | Pattern.dispatch(shouldmatch, plugin) 167 | assert.are.equal(1, num_matches) 168 | 169 | num_matches = 0 170 | osc:remove_handler('/foo/{bar,baz}/123') 171 | osc:add_handler('/foo/{bar,baz}/{x,y,z}/123', function(data) 172 | assert.not_nil(data) 173 | num_matches = num_matches + 1 174 | end) 175 | 176 | shouldmatch = Message.pack({address = '/foo/baz/z/123', types = 'i', 1}) 177 | nomatch = Message.pack({address = '/foo/baz/q/123', types = 'i', 1}) 178 | Pattern.dispatch(nomatch, plugin) 179 | assert.are.equal(0, num_matches) 180 | Pattern.dispatch(shouldmatch, plugin) 181 | assert.are.equal(1, num_matches) 182 | end) 183 | end) 184 | end) 185 | -------------------------------------------------------------------------------- /spec/timetag_spec.lua: -------------------------------------------------------------------------------- 1 | local Timetag = require'losc.timetag' 2 | 3 | describe('Timetag', function() 4 | describe('constructors', function() 5 | it('can create a raw timetag', function() 6 | local tt = Timetag.new_raw() 7 | assert.not_nil(tt) 8 | assert.are.equal(0, tt:timestamp()) 9 | assert.are.equal(0, tt:seconds()) 10 | assert.are.equal(1, tt:fractions()) 11 | tt = Timetag.new_raw(0) 12 | assert.are.equal(1, tt:fractions()) 13 | tt = Timetag.new_raw(1, 2) 14 | assert.are.equal(1, tt:seconds()) 15 | assert.are.equal(2, tt:fractions()) 16 | end) 17 | 18 | it('can create a timetag from seconds and fractions', function() 19 | local now = os.time() 20 | local tt = Timetag.new(now, 5000) -- default is microsecond precision 21 | assert.not_nil(tt) 22 | assert.are.equal((now + 5) * 1000, tt:timestamp()) 23 | end) 24 | 25 | it('can create a timetag from a timestamp', function() 26 | local now = os.time() 27 | local tt = Timetag.new(now, 1234) 28 | local tt2 = Timetag.new_from_timestamp(tt:timestamp()) 29 | assert.are.equal(tt:timestamp(), tt2:timestamp()) 30 | end) 31 | end) 32 | 33 | describe('pack', function() 34 | it('packs correct byte representation', function() 35 | -- create a timetag with special value "now" 36 | local tt = Timetag.new() 37 | local data = Timetag.pack(tt.content) 38 | assert.are.equal(0, #data % 4) 39 | assert.are.equal('\0\0\0\0\0\0\0\1', data) 40 | end) 41 | end) 42 | 43 | describe('unpack', function() 44 | it('unpacks the correct value', function() 45 | local now = os.time() 46 | local tt = Timetag.new(now) 47 | local data = Timetag.pack(tt.content) 48 | local value = Timetag.unpack(data) 49 | assert.not_nil(value) 50 | assert.is_true(value.seconds > now) 51 | assert.are.equal(0, value.fractions) 52 | assert.are.equal(now, value.seconds - 2208988800) 53 | end) 54 | end) 55 | 56 | describe('methods', function() 57 | it('returns a timestamp with arbitrary precision', function() 58 | local now = os.time() 59 | local tt = Timetag.new(now) -- default is milliseconds 60 | assert.are.equal(now * 1000, tt:timestamp(1000)) 61 | tt = Timetag.new(now, 0, 1000000) -- microseconds 62 | assert.are.equal(now * 1e6, tt:timestamp(1e6)) 63 | tt = Timetag.new(now, 0, 1) -- seconds 64 | assert.are.equal(now, tt:timestamp(1)) 65 | end) 66 | 67 | it('has overloaded add operator', function() 68 | local now = os.time() 69 | local tt = Timetag.new(now, 0, 1) 70 | tt = tt + 1 71 | assert.are.equal(now + 1, tt:timestamp(1)) 72 | tt = 1 + tt 73 | assert.are.equal(now + 2, tt:timestamp(1)) 74 | local tt2 = tt + 10 75 | assert.are.not_equal(tt, tt2) 76 | end) 77 | 78 | it('can access seconds and fractions', function() 79 | local now = os.time() 80 | local tt = Timetag.new(now) 81 | assert.is_true(type(tt:seconds()) == 'number') 82 | assert.is_true(type(tt:fractions()) == 'number') 83 | end) 84 | end) 85 | end) 86 | -------------------------------------------------------------------------------- /spec/types/blob_spec.lua: -------------------------------------------------------------------------------- 1 | local Types = require'losc.types' 2 | 3 | describe('blob', function() 4 | local value = 'helloworld' 5 | local expected_bytes = {0, 0, 0, 10, 104, 101, 108, 108, 111, 119, 111, 114, 108, 100, 0, 0} 6 | local data 7 | 8 | describe('pack', function() 9 | setup(function() 10 | data = Types.pack.b(value) 11 | end) 12 | 13 | it('returns the correct byte representation', function() 14 | local bytes = {string.byte(data, 1, -1)} 15 | assert.are.equal(#bytes, #expected_bytes) 16 | for i, byte in ipairs(bytes) do 17 | assert.are.equal(expected_bytes[i], byte) 18 | end 19 | end) 20 | 21 | it('returns a multiple of 32', function() 22 | assert.are.equal(#data * 8 % 32, 0) 23 | end) 24 | end) 25 | 26 | describe('unpack', function() 27 | local blob, offset 28 | 29 | setup(function() 30 | blob, offset = Types.unpack.b(data) 31 | end) 32 | 33 | it('returns the correct offset', function() 34 | assert.are.equal(offset, 17) 35 | end) 36 | 37 | it('returns the correct value', function() 38 | assert.are.equal(value, blob) 39 | end) 40 | end) 41 | end) 42 | -------------------------------------------------------------------------------- /spec/types/double_spec.lua: -------------------------------------------------------------------------------- 1 | local Types = require'losc.types' 2 | 3 | describe('double', function() 4 | local value = 1.234567891234 5 | local expected_bytes = {63, 243, 192, 202, 66, 216, 170, 221} 6 | local data 7 | 8 | describe('pack', function() 9 | setup(function() 10 | data = Types.pack.d(value) 11 | end) 12 | 13 | it('returns the correct byte representation', function() 14 | local bytes = {string.byte(data, 1, -1)} 15 | for i, byte in ipairs(bytes) do 16 | assert.are.equal(expected_bytes[i], byte) 17 | end 18 | end) 19 | 20 | it('returns a multiple of 32', function() 21 | assert.are.equal(#data * 8 % 32, 0) 22 | end) 23 | end) 24 | 25 | describe('unpack', function() 26 | local d, offset 27 | 28 | setup(function() 29 | d, offset = Types.unpack.d(data) 30 | end) 31 | 32 | it('returns the correct offset', function() 33 | assert.are.equal(offset, 9) 34 | end) 35 | 36 | it('returns the correct value', function() 37 | local res = math.abs(d - value) < 1e-7 38 | assert.are.is_true(res) 39 | end) 40 | end) 41 | end) 42 | 43 | local test_int64 = function() 44 | local data, expected_bytes 45 | local value = 9223372036854775807 46 | expected_bytes = {127, 255, 255, 255, 255, 255, 255, 255} 47 | 48 | describe('pack', function() 49 | setup(function() 50 | data = Types.pack.h(value) 51 | end) 52 | 53 | it('returns the correct byte representation', function() 54 | local bytes = {string.byte(data, 1, -1)} 55 | for i, byte in ipairs(bytes) do 56 | assert.are.equal(expected_bytes[i], byte) 57 | end 58 | end) 59 | 60 | it('returns a multiple of 32', function() 61 | assert.are.equal(#data * 8 % 32, 0) 62 | end) 63 | end) 64 | 65 | describe('unpack', function() 66 | local h, offset 67 | 68 | setup(function() 69 | h, offset = Types.unpack.h(data) 70 | end) 71 | 72 | it('returns the correct offset', function() 73 | assert.are.equal(offset, 9) 74 | end) 75 | 76 | it('returns the correct value', function() 77 | assert.are.equal(value, h) 78 | end) 79 | end) 80 | end 81 | 82 | -- https://github.com/iryont/lua-struct/issues/3 83 | -- lua < 5.1 can not represent this type. 84 | describe('int64', string.pack and test_int64 or function() end) 85 | -------------------------------------------------------------------------------- /spec/types/extended_spec.lua: -------------------------------------------------------------------------------- 1 | local Types = require'losc.types' 2 | 3 | describe('Extended types', function() 4 | describe('No payload types', function() 5 | local value = '' 6 | local index = 1 7 | 8 | it('can unpack T (boolean)', function() 9 | value, index = Types.unpack.T(value, index) 10 | assert.is.equal(true, value) 11 | assert.are.equal(1, index) 12 | end) 13 | 14 | it('can unpack F (boolean)', function() 15 | value, index = Types.unpack.F(value, index) 16 | assert.is.equal(false, value) 17 | assert.are.equal(1, index) 18 | end) 19 | 20 | it('can unpack N (nil)', function() 21 | value, index = Types.unpack.N(value, index) 22 | assert.is.equal(false, value) 23 | assert.are.equal(1, index) 24 | end) 25 | 26 | it('can unpack I (infinitum)', function() 27 | value, index = Types.unpack.I(value, index) 28 | assert.is.equal(math.huge, value) 29 | assert.are.equal(1, index) 30 | end) 31 | end) 32 | end) 33 | -------------------------------------------------------------------------------- /spec/types/float_spec.lua: -------------------------------------------------------------------------------- 1 | local Types = require'losc.types' 2 | 3 | describe('float', function() 4 | local value = 1.2345678 5 | local expected_bytes = { 63, 158, 6, 81 } 6 | local data 7 | 8 | describe('pack', function() 9 | setup(function() 10 | data = Types.pack.f(value) 11 | end) 12 | 13 | it('returns the correct byte representation', function() 14 | local bytes = {string.byte(data, 1, -1)} 15 | for i, byte in ipairs(bytes) do 16 | assert.are.equal(expected_bytes[i], byte) 17 | end 18 | end) 19 | 20 | it('returns a multiple of 32', function() 21 | assert.are.equal(#data * 8 % 32, 0) 22 | end) 23 | end) 24 | 25 | describe('unpack', function() 26 | local f, offset 27 | 28 | setup(function() 29 | f, offset = Types.unpack.f(data) 30 | end) 31 | 32 | it('returns the correct offset', function() 33 | assert.are.equal(offset, 5) 34 | end) 35 | 36 | it('returns the correct value', function() 37 | local res = math.abs(f - value) < 1e-7 38 | assert.are.is_true(res) 39 | end) 40 | end) 41 | end) 42 | -------------------------------------------------------------------------------- /spec/types/int32_spec.lua: -------------------------------------------------------------------------------- 1 | local Types = require'losc.types' 2 | 3 | describe('int32', function() 4 | local value = 123456789 5 | local expected_bytes = {7, 91, 205, 21} 6 | local data 7 | 8 | describe('pack', function() 9 | setup(function() 10 | data = Types.pack.i(value) 11 | end) 12 | 13 | it('returns the correct byte representation', function() 14 | local bytes = {string.byte(data, 1, -1)} 15 | for i, byte in ipairs(bytes) do 16 | assert.are.equal(expected_bytes[i], byte) 17 | end 18 | end) 19 | 20 | it('returns a multiple of 32', function() 21 | assert.are.equal(#data * 8 % 32, 0) 22 | end) 23 | end) 24 | 25 | describe('unpack', function() 26 | local i, offset 27 | 28 | setup(function() 29 | i, offset = Types.unpack.i(data) 30 | end) 31 | 32 | it('returns the correct offset', function() 33 | assert.are.equal(offset, 5) 34 | end) 35 | 36 | it('returns the correct value', function() 37 | assert.are.equal(i, value) 38 | end) 39 | end) 40 | end) 41 | -------------------------------------------------------------------------------- /spec/types/string_spec.lua: -------------------------------------------------------------------------------- 1 | local Types = require'losc.types' 2 | 3 | describe('string', function() 4 | local value = 'hello' 5 | local expected_bytes = {104, 101, 108, 108, 111, 0, 0, 0} 6 | local data 7 | 8 | describe('pack', function() 9 | setup(function() 10 | data = Types.pack.s(value) 11 | end) 12 | 13 | it('returns the correct byte representation', function() 14 | local bytes = {string.byte(data, 1, -1)} 15 | assert.are.equal(#bytes, #expected_bytes) 16 | for i, byte in ipairs(bytes) do 17 | assert.are.equal(expected_bytes[i], byte) 18 | end 19 | end) 20 | 21 | it('returns a multiple of 32', function() 22 | assert.are.equal(#data * 8 % 32, 0) 23 | end) 24 | end) 25 | 26 | describe('unpack', function() 27 | local s, offset 28 | 29 | setup(function() 30 | s, offset = Types.unpack.s(data) 31 | end) 32 | 33 | it('returns the correct offset', function() 34 | assert.are.equal(offset, 9) 35 | end) 36 | 37 | it('returns the correct value', function() 38 | assert.are.equal(value, s) 39 | end) 40 | end) 41 | end) 42 | -------------------------------------------------------------------------------- /spec/types_spec.lua: -------------------------------------------------------------------------------- 1 | local Types = require'losc.types' 2 | 3 | describe('Types', function() 4 | local types = 'ifsb' 5 | local packed_data = {} 6 | local data = { 7 | i = 123456789, 8 | f = 1.2345678, 9 | s = 'hello', 10 | b = 'world123' 11 | } 12 | 13 | describe('pack', function() 14 | it('can handle the fundamental types', function() 15 | for type in types:gmatch('.') do 16 | assert.not_nil(Types.pack[type]) 17 | end 18 | end) 19 | 20 | it('returns a multiple of 32', function() 21 | for k, v in pairs(data) do 22 | local buffer = Types.pack[k](v) 23 | assert.are.equal(#buffer * 8 % 32, 0) 24 | packed_data[k] = buffer 25 | end 26 | end) 27 | 28 | it('can be called as function', function() 29 | local ok, data = Types.pack('s', 'hello') 30 | assert.is_true(ok) 31 | assert.are.equal('hello\0\0\0', data) 32 | ok, data = Types.pack('s', 123) 33 | assert.is_false(ok) 34 | end) 35 | 36 | it('has types which will be skipped in packing', function() 37 | assert.is_nil(Types.pack['T']) 38 | assert.is_nil(Types.pack['F']) 39 | assert.is_nil(Types.pack['N']) 40 | assert.is_nil(Types.pack['I']) 41 | end) 42 | end) 43 | 44 | describe('unpack', function() 45 | it('can handle the fundamental types', function() 46 | for type in types:gmatch('.') do 47 | assert.not_nil(Types.unpack[type]) 48 | end 49 | end) 50 | 51 | it('returns the correct value', function() 52 | for k, v in pairs(packed_data) do 53 | local value, offset, size 54 | if k == 'f' then 55 | value, offset = Types.unpack[k](v) 56 | assert.is_true(math.abs(data[k] - value) < 1e-6) 57 | else 58 | value, offset = Types.unpack[k](v) 59 | assert.are.equal(data[k], value) 60 | end 61 | end 62 | end) 63 | 64 | it('can be called as function', function() 65 | local data = 'hello\0\0\0' 66 | local ok, value = Types.unpack('s', data, 1) 67 | assert.is_true(ok) 68 | assert.are.equal('hello', value) 69 | ok, data = Types.unpack('s', nil, 1) 70 | assert.is_false(ok) 71 | end) 72 | 73 | it('can get supported types', function() 74 | local ptypes = Types.get(Types.pack) 75 | local utypes = Types.get(Types.unpack) 76 | assert.is_true(type(ptypes) == 'table') 77 | assert.is_true(type(utypes) == 'table') 78 | end) 79 | end) 80 | end) 81 | -------------------------------------------------------------------------------- /src/losc/bundle.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | -------------- 26 | -- OSC Bundle. 27 | -- 28 | -- An OSC Bundle consists of the OSC-string "#bundle" followed by an OSC Time 29 | -- Tag, followed by zero or more OSC Bundle Elements. 30 | -- 31 | -- @module losc.bundle 32 | -- @author David Granström 33 | -- @license MIT 34 | -- @copyright David Granström 2021 35 | 36 | local relpath = (...):gsub('%.[^%.]+$', '') 37 | local Types = require(relpath .. '.types') 38 | local Message = require(relpath .. '.message') 39 | local Timetag = require(relpath .. '.timetag') 40 | 41 | local Bundle = {} 42 | Bundle.__index = Bundle 43 | 44 | local ts = Timetag.get_timestamp 45 | 46 | --- Pack a Bundle recursively. 47 | local function _pack(bundle, packet) 48 | packet[#packet + 1] = Types.pack.s('#bundle') 49 | packet[#packet + 1] = Types.pack.t(bundle.timetag) 50 | for _, item in ipairs(bundle) do 51 | if item.address and item.types then 52 | local message = Message.pack(item) 53 | packet[#packet + 1] = Types.pack.i(#message) 54 | packet[#packet + 1] = message 55 | elseif item.timetag then 56 | if ts(item.timetag) < ts(bundle.timetag) then 57 | error('Bundle timetag is less than enclosing bundle.') 58 | end 59 | local bndl = Bundle.pack(item) 60 | packet[#packet + 1] = Types.pack.i(#bndl) 61 | packet[#packet + 1] = bndl 62 | end 63 | end 64 | return table.concat(packet, '') 65 | end 66 | 67 | --- Unpack a Bundle recursively. 68 | local function _unpack(data, bundle, offset, length) 69 | local value, _ 70 | _, offset = Types.unpack.s(data, offset) 71 | value, offset = Types.unpack.t(data, offset) 72 | bundle.timetag = value 73 | length = length or #data 74 | while offset < length do 75 | -- content length 76 | value, offset = Types.unpack.i(data, offset) 77 | local head = data:sub(offset, offset) 78 | if head == '#' then 79 | value, offset = _unpack(data, {}, offset, offset + value - 1) 80 | bundle[#bundle + 1] = value 81 | elseif head == '/' then 82 | value, offset = Message.unpack(data, offset) 83 | bundle[#bundle + 1] = value 84 | end 85 | end 86 | return bundle, offset 87 | end 88 | 89 | --- High level API 90 | -- @section high-level-api 91 | 92 | --- Create a new OSC bundle. 93 | -- 94 | -- Arguments can be one form of: 95 | -- 96 | -- 1. nil (return empty bundle object). 97 | -- 2. Timetag. 98 | -- 3. Timetag, message/bundle objects. 99 | -- 100 | -- @param[opt] ... arguments. 101 | -- @return Bundle object. 102 | function Bundle.new(...) 103 | local self = setmetatable({}, Bundle) 104 | local args = {...} 105 | self.content = {} 106 | if #args >= 1 then 107 | self.content.timetag = args[1].content 108 | for index = 2, #args do 109 | self:add(args[index]) 110 | end 111 | end 112 | return self 113 | end 114 | 115 | --- Adds an item to the bundle. 116 | -- @param item Can be a Message or another bundle. 117 | function Bundle:add(item) 118 | self.content[#self.content + 1] = item.content 119 | end 120 | 121 | --- Get or set the bundle Timetag. 122 | -- @param[opt] tt A Timetag object. 123 | -- If no parameter is given it returns the current Timetag. 124 | function Bundle:timetag(tt) 125 | if tt then 126 | self.content.timetag = tt.content 127 | else 128 | return Timetag.new_raw(self.content.timetag) 129 | end 130 | end 131 | 132 | --- Validate a bundle. 133 | -- @tparam table|string bundle The bundle to validate. Can be in packed or unpacked form. 134 | function Bundle.validate(bundle) 135 | assert(bundle) 136 | if type(bundle) == 'string' then 137 | Bundle.bytes_validate(bundle) 138 | elseif type(bundle) == 'table' then 139 | Bundle.tbl_validate(bundle.content or bundle) 140 | end 141 | end 142 | 143 | --- Low level API 144 | -- @section low-level-api 145 | 146 | --- Validate a table that can be used as an OSC bundle. 147 | -- @param tbl The table to validate. 148 | function Bundle.tbl_validate(tbl) 149 | assert(type(tbl.timetag) == 'table', 'Missing OSC Timetag.') 150 | end 151 | 152 | --- Validate a byte string that can be unpacked to an OSC bundle. 153 | -- @param data The byte string to validate. 154 | -- @param[opt] offset Byte offset. 155 | function Bundle.bytes_validate(data, offset) 156 | local value 157 | assert(#data % 4 == 0, 'OSC bundle data must be a multiple of 4.') 158 | value, offset = Types.unpack.s(data, offset or 1) 159 | assert(value == '#bundle', 'Missing bundle marker') 160 | value = Types.unpack.t(data, offset) 161 | assert(type(value) == 'table', 'Missing bundle timetag') 162 | end 163 | 164 | --- Pack an OSC bundle. 165 | -- 166 | -- The returned object is suitable for sending via a transport layer such as 167 | -- UDP or TCP. 168 | -- 169 | -- @param tbl The content to pack. 170 | -- @return OSC data packet (byte string). 171 | function Bundle.pack(tbl) 172 | local packet = {} 173 | return _pack(tbl, packet) 174 | end 175 | 176 | --- Unpack an OSC bundle byte string. 177 | -- 178 | -- @param data The data to unpack. 179 | -- @param offset The initial offset into data. 180 | -- @return table with the content of the OSC bundle. 181 | function Bundle.unpack(data, offset) 182 | local bundle = {} 183 | return _unpack(data, bundle, offset or 1) 184 | end 185 | 186 | return Bundle 187 | -------------------------------------------------------------------------------- /src/losc/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | ------------------ 26 | -- High level API. 27 | -- 28 | -- Main module of losc. 29 | -- 30 | -- @module losc 31 | -- @author David Granström 32 | -- @license MIT 33 | -- @copyright David Granström 2021 34 | 35 | local relpath = (...):gsub('%.init$', '') 36 | local Message = require(relpath ..'.message') 37 | local Bundle = require(relpath .. '.bundle') 38 | local Timetag = require(relpath .. '.timetag') 39 | local Pattern = require(relpath .. '.pattern') 40 | 41 | local losc = { 42 | _VERSION = 'losc v1.0.1', 43 | _URL = 'https://github.com/davidgranstrom/losc', 44 | _DESCRIPTION = 'Open Sound Control (OSC) library for lua/luajit.', 45 | _LICENSE = [[ 46 | MIT License 47 | 48 | Copyright (c) 2021 David Granström 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy 51 | of this software and associated documentation files (the "Software"), to deal 52 | in the Software without restriction, including without limitation the rights 53 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 54 | copies of the Software, and to permit persons to whom the Software is 55 | furnished to do so, subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in all 58 | copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 62 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 63 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 64 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 65 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 66 | SOFTWARE. 67 | ]] 68 | } 69 | losc.__index = losc 70 | 71 | --- Create a new instance. 72 | -- @tparam[options] table options Options. 73 | -- @usage local osc = losc.new() 74 | -- @usage local osc = losc.new {plugin = plugin.new()} 75 | function losc.new(options) 76 | local self = setmetatable({}, losc) 77 | self.handlers = {} 78 | if options then 79 | if options.plugin then 80 | self:use(options.plugin) 81 | end 82 | end 83 | return self 84 | end 85 | 86 | --- Create a new Message. 87 | -- @tparam[opt] string|table args OSC address or table constructor. 88 | -- @return message object 89 | -- @see losc.message 90 | -- @usage local message = losc.new_message() 91 | -- @usage local message = losc.new_message('/address') 92 | -- @usage local message = losc.new_message{address = '/test', types = 'iif', 1, 2, 3} 93 | function losc.new_message(args) 94 | local ok, message = pcall(Message.new, args) 95 | if not ok then 96 | error(message) 97 | end 98 | return message 99 | end 100 | 101 | --- Create a new OSC bundle. 102 | -- @param[opt] ... arguments. 103 | -- @return bundle object 104 | -- @see losc.bundle 105 | -- @usage 106 | -- local tt = losc:now() 107 | -- local bundle = losc.new_bundle() 108 | -- bundle:timetag(tt) 109 | -- -- packet can be a message or another bundle 110 | -- bundle:add(packet) 111 | -- @usage 112 | -- local tt = losc:now() 113 | -- local bundle = losc.new_bundle(tt) 114 | -- bundle:add(packet) 115 | -- @usage 116 | -- local tt = losc:now() 117 | -- local bundle = losc.new_bundle(tt, packet, packet2) 118 | function losc.new_bundle(...) 119 | local ok, bundle = pcall(Bundle.new, ...) 120 | if not ok then 121 | error(bundle) 122 | end 123 | return bundle 124 | end 125 | 126 | --- Specify a plugin to use as transport layer. 127 | -- @param plugin The plugin to use, pass nil to disable current plugin. 128 | function losc:use(plugin) 129 | self.plugin = plugin 130 | if plugin then 131 | self.plugin.handlers = self.handlers 132 | end 133 | end 134 | 135 | --- Get an OSC timetag with the current timestamp. 136 | -- Will fall back to `os.time()` if `now()` is not implemented by the plugin 137 | -- in use. 138 | -- @usage local tt = losc:now() 139 | -- @usage 140 | -- -- 0.25 seconds into the future. 141 | -- local tt = losc:now() + 0.25 142 | function losc:now() 143 | if self.plugin and self.plugin.now then 144 | return self.plugin:now() 145 | end 146 | return Timetag.new(os.time(), 0) 147 | end 148 | 149 | --- Opens an OSC server. 150 | -- This function might be blocking depending on the plugin in use. 151 | -- @param[opt] ... Plugin specific arguments. 152 | -- @return status, plugin handle or error 153 | -- @usage losc:open() 154 | function losc:open(...) 155 | if not self.plugin then 156 | error('"open" must be implemented using a plugin.') 157 | end 158 | return pcall(self.plugin.open, self.plugin, ...) 159 | end 160 | 161 | --- Closes an OSC server. 162 | -- @param[opt] ... Plugin specific arguments. 163 | -- @return status, nil or error 164 | -- @usage losc:close() 165 | function losc:close(...) 166 | if not self.plugin then 167 | error('"close" must be implemented using a plugin.') 168 | end 169 | return pcall(self.plugin.close, self.plugin, ...) 170 | end 171 | 172 | --- Send an OSC packet. 173 | -- @param[opt] ... Plugin specific arguments. 174 | -- @return status, nil or error 175 | -- @usage 176 | -- -- can be message or bundle. 177 | -- local packet = losc.new_message{address = '/x', types = 'i', 1} 178 | -- losc:send(packet) 179 | -- -- additional plugin arguments (can vary between plugins) 180 | -- losc:send(packet, 'localhost', 9000) 181 | function losc:send(...) 182 | if not self.plugin then 183 | error('"send" must be implemented using a plugin.') 184 | end 185 | return pcall(self.plugin.send, self.plugin, ...) 186 | end 187 | 188 | --- Add an OSC handler. 189 | -- @param pattern The pattern to match on. 190 | -- @param func The callback to run if a message is received. 191 | -- The callback will get a single argument `data` from where the messsage can be retrived. 192 | -- @usage 193 | -- osc:add_handler('/pattern', function(data) 194 | -- -- message table, can be converted to Message if needed. 195 | -- local message = data.message 196 | -- -- timestamp when message was received, can be converted to Timetag if needed. 197 | -- local timestamp = data.timestamp 198 | -- -- table with remote (sender) info, can be empty depending on plugin. 199 | -- local remote_info = data.remote_info 200 | -- end) 201 | -- @usage 202 | -- osc:add_handler('/pattern', function(data) 203 | -- -- arguments can be accessed by index from the message table 204 | -- local arg1 = data.message[1] 205 | -- local arg2 = data.message[2] 206 | -- -- iterate over incoming OSC arguments 207 | -- for _, argument in ipairs(data.message) do 208 | -- print(argument) 209 | -- end 210 | -- end) 211 | -- @usage 212 | -- -- Pattern matching (groups) 213 | -- osc:add_handler('/param/{x,y}/123', function(data) end) 214 | -- -- Pattern matching (sequence) 215 | -- osc:add_handler('/param/[a-f]/123', function(data) end) 216 | -- -- Pattern matching (sequence) 217 | -- osc:add_handler('/param/[!a-f]/123', function(data) end) 218 | -- -- Pattern matching (wildcard) 219 | -- osc:add_handler('/param/*/123', function(data) end) 220 | -- osc:add_handler('*', function(data) end) 221 | function losc:add_handler(pattern, func) 222 | self.handlers[pattern] = { 223 | pattern = Pattern.escape(pattern), 224 | callback = func, 225 | } 226 | if self.plugin then 227 | self.plugin.handlers = self.handlers 228 | end 229 | end 230 | 231 | --- Remove an OSC handler. 232 | -- @param pattern The pattern for the handler to remove. 233 | -- @usage losc:remove_handler('/handler/to/remove') 234 | function losc:remove_handler(pattern) 235 | self.handlers[pattern] = nil 236 | if self.plugin then 237 | self.plugin.handlers[pattern] = nil 238 | end 239 | end 240 | 241 | --- Remove all registered OSC handlers. 242 | -- @usage losc:remove_all() 243 | function losc:remove_all() 244 | self.handlers = {} 245 | if self.plugin then 246 | self.plugin.handlers = {} 247 | end 248 | end 249 | 250 | return losc 251 | -------------------------------------------------------------------------------- /src/losc/lib/struct.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * Copyright (c) 2015-2020 Iryont 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | ]] 22 | 23 | local unpack = table.unpack or _G.unpack 24 | 25 | local struct = {} 26 | 27 | function struct.pack(format, ...) 28 | local stream = {} 29 | local vars = {...} 30 | local endianness = true 31 | 32 | for i = 1, format:len() do 33 | local opt = format:sub(i, i) 34 | 35 | if opt == '<' then 36 | endianness = true 37 | elseif opt == '>' then 38 | endianness = false 39 | elseif opt:find('[bBhHiIlL]') then 40 | local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1 41 | local val = tonumber(table.remove(vars, 1)) 42 | 43 | local bytes = {} 44 | for j = 1, n do 45 | table.insert(bytes, string.char(val % (2 ^ 8))) 46 | val = math.floor(val / (2 ^ 8)) 47 | end 48 | 49 | if not endianness then 50 | table.insert(stream, string.reverse(table.concat(bytes))) 51 | else 52 | table.insert(stream, table.concat(bytes)) 53 | end 54 | elseif opt:find('[fd]') then 55 | local val = tonumber(table.remove(vars, 1)) 56 | local sign = 0 57 | 58 | if val < 0 then 59 | sign = 1 60 | val = -val 61 | end 62 | 63 | local mantissa, exponent = math.frexp(val) 64 | if val == 0 then 65 | mantissa = 0 66 | exponent = 0 67 | else 68 | mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, (opt == 'd') and 53 or 24) 69 | exponent = exponent + ((opt == 'd') and 1022 or 126) 70 | end 71 | 72 | local bytes = {} 73 | if opt == 'd' then 74 | val = mantissa 75 | for i = 1, 6 do 76 | table.insert(bytes, string.char(math.floor(val) % (2 ^ 8))) 77 | val = math.floor(val / (2 ^ 8)) 78 | end 79 | else 80 | table.insert(bytes, string.char(math.floor(mantissa) % (2 ^ 8))) 81 | val = math.floor(mantissa / (2 ^ 8)) 82 | table.insert(bytes, string.char(math.floor(val) % (2 ^ 8))) 83 | val = math.floor(val / (2 ^ 8)) 84 | end 85 | 86 | table.insert(bytes, string.char(math.floor(exponent * ((opt == 'd') and 16 or 128) + val) % (2 ^ 8))) 87 | val = math.floor((exponent * ((opt == 'd') and 16 or 128) + val) / (2 ^ 8)) 88 | table.insert(bytes, string.char(math.floor(sign * 128 + val) % (2 ^ 8))) 89 | val = math.floor((sign * 128 + val) / (2 ^ 8)) 90 | 91 | if not endianness then 92 | table.insert(stream, string.reverse(table.concat(bytes))) 93 | else 94 | table.insert(stream, table.concat(bytes)) 95 | end 96 | elseif opt == 's' then 97 | table.insert(stream, tostring(table.remove(vars, 1))) 98 | table.insert(stream, string.char(0)) 99 | elseif opt == 'c' then 100 | local n = format:sub(i + 1):match('%d+') 101 | local str = tostring(table.remove(vars, 1)) 102 | local len = tonumber(n) 103 | if len <= 0 then 104 | len = str:len() 105 | end 106 | if len - str:len() > 0 then 107 | str = str .. string.rep(' ', len - str:len()) 108 | end 109 | table.insert(stream, str:sub(1, len)) 110 | i = i + n:len() 111 | end 112 | end 113 | 114 | return table.concat(stream) 115 | end 116 | 117 | function struct.unpack(format, stream, pos) 118 | local vars = {} 119 | local iterator = pos or 1 120 | local endianness = true 121 | 122 | for i = 1, format:len() do 123 | local opt = format:sub(i, i) 124 | 125 | if opt == '<' then 126 | endianness = true 127 | elseif opt == '>' then 128 | endianness = false 129 | elseif opt:find('[bBhHiIlL]') then 130 | local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1 131 | local signed = opt:lower() == opt 132 | 133 | local val = 0 134 | for j = 1, n do 135 | local byte = string.byte(stream:sub(iterator, iterator)) 136 | if endianness then 137 | val = val + byte * (2 ^ ((j - 1) * 8)) 138 | else 139 | val = val + byte * (2 ^ ((n - j) * 8)) 140 | end 141 | iterator = iterator + 1 142 | end 143 | 144 | if signed and val >= 2 ^ (n * 8 - 1) then 145 | val = val - 2 ^ (n * 8) 146 | end 147 | 148 | table.insert(vars, math.floor(val)) 149 | elseif opt:find('[fd]') then 150 | local n = (opt == 'd') and 8 or 4 151 | local x = stream:sub(iterator, iterator + n - 1) 152 | iterator = iterator + n 153 | 154 | if not endianness then 155 | x = string.reverse(x) 156 | end 157 | 158 | local sign = 1 159 | local mantissa = string.byte(x, (opt == 'd') and 7 or 3) % ((opt == 'd') and 16 or 128) 160 | for i = n - 2, 1, -1 do 161 | mantissa = mantissa * (2 ^ 8) + string.byte(x, i) 162 | end 163 | 164 | if string.byte(x, n) > 127 then 165 | sign = -1 166 | end 167 | 168 | local exponent = (string.byte(x, n) % 128) * ((opt == 'd') and 16 or 2) + math.floor(string.byte(x, n - 1) / ((opt == 'd') and 16 or 128)) 169 | if exponent == 0 then 170 | table.insert(vars, 0.0) 171 | else 172 | mantissa = (math.ldexp(mantissa, (opt == 'd') and -52 or -23) + 1) * sign 173 | table.insert(vars, math.ldexp(mantissa, exponent - ((opt == 'd') and 1023 or 127))) 174 | end 175 | elseif opt == 's' then 176 | local bytes = {} 177 | for j = iterator, stream:len() do 178 | if stream:sub(j,j) == string.char(0) or stream:sub(j) == '' then 179 | break 180 | end 181 | 182 | table.insert(bytes, stream:sub(j, j)) 183 | end 184 | 185 | local str = table.concat(bytes) 186 | iterator = iterator + str:len() + 1 187 | table.insert(vars, str) 188 | elseif opt == 'c' then 189 | local n = format:sub(i + 1):match('%d+') 190 | local len = tonumber(n) 191 | if len <= 0 then 192 | len = table.remove(vars) 193 | end 194 | 195 | table.insert(vars, stream:sub(iterator, iterator + len - 1)) 196 | iterator = iterator + len 197 | i = i + n:len() 198 | end 199 | end 200 | table.insert(vars, iterator) 201 | return unpack(vars) 202 | end 203 | 204 | return struct 205 | -------------------------------------------------------------------------------- /src/losc/message.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | --------------- 26 | -- OSC Message. 27 | -- 28 | -- An OSC message consists of an OSC Address Pattern followed by an 29 | -- OSC Type Tag String followed by zero or more OSC Arguments. 30 | -- 31 | -- @module losc.message 32 | -- @author David Granström 33 | -- @license MIT 34 | -- @copyright David Granström 2021 35 | 36 | local relpath = (...):gsub('%.[^%.]+$', '') 37 | local Types = require(relpath .. '.types') 38 | 39 | local Message = {} 40 | Message.__index = Message 41 | 42 | --- High level API 43 | -- @section high-level-api 44 | 45 | --- Create a new OSC message. 46 | -- 47 | -- @tparam[opt] string|table msg OSC address or table constructor. 48 | -- 49 | -- @return An OSC message object. 50 | -- @see losc.types 51 | -- @usage 52 | -- local msg = Message.new() 53 | -- @usage 54 | -- local msg = Message.new('/some/addr') 55 | -- @usage 56 | -- local tbl = {address = '/some/addr', types = 'ifs', 1, 2.0, 'hi'} 57 | -- local msg = Message.new(tbl) 58 | function Message.new(msg) 59 | local self = setmetatable({}, Message) 60 | self.content = {} 61 | self.content.address = '' 62 | self.content.types = '' 63 | if msg then 64 | if type(msg) == 'string' then 65 | if msg:sub(1,1) ~= '/' then 66 | msg = '/' .. msg 67 | end 68 | Message.address_validate(msg) 69 | self.content.address = msg 70 | elseif type(msg) == 'table' then 71 | Message.tbl_validate(msg) 72 | self.content = msg 73 | end 74 | end 75 | return self 76 | end 77 | 78 | --- Add arguments to the message. 79 | -- 80 | -- @param type OSC type string. 81 | -- @param[opt] item Item to add. 82 | -- @see losc.types 83 | -- @usage message:add('i', 123) 84 | -- @usage message:add('T') 85 | function Message:add(type, item) 86 | self.content.types = self.content.types .. type 87 | if item then 88 | self.content[#self.content + 1] = item 89 | else 90 | if type == 'T' or type == 'F' then 91 | self.content[#self.content + 1] = type == 'T' 92 | elseif type == 'N' then 93 | self.content[#self.content + 1] = false 94 | elseif type == 'I' then 95 | self.content[#self.content + 1] = math.huge 96 | end 97 | end 98 | end 99 | 100 | --- Message iterator. 101 | -- 102 | -- Iterate over message types and arguments. 103 | -- 104 | -- @return iterator using index, type, argument. 105 | -- @usage for i, type, arg in message:iter() do 106 | -- print(i, type, arg) 107 | -- end 108 | function Message:iter() 109 | local tbl = {self.content.types, self.content} 110 | local function msg_it(t, i) 111 | i = i + 1 112 | local type = t[1]:sub(i, i) 113 | local arg = t[2][i] 114 | if type ~= nil and arg ~= nil then 115 | return i, type, arg 116 | end 117 | return nil 118 | end 119 | return msg_it, tbl, 0 120 | end 121 | 122 | --- Get the OSC address. 123 | -- @return The OSC address. 124 | function Message:address() 125 | return self.content.address 126 | end 127 | 128 | --- Get the OSC type string. 129 | -- @return OSC type string or empty string. 130 | function Message:types() 131 | return self.content.types 132 | end 133 | 134 | --- Get the OSC arguments. 135 | -- @return Table with arguments. 136 | function Message:args() 137 | local args = {} 138 | for _, a in ipairs(self.content) do 139 | args[#args + 1] = a 140 | end 141 | return args 142 | end 143 | 144 | --- Validate a message. 145 | -- @tparam table|string message The message to validate. Can be in packed or unpacked form. 146 | function Message.validate(message) 147 | assert(message) 148 | if type(message) == 'string' then 149 | Message.bytes_validate(message) 150 | elseif type(message) == 'table' then 151 | Message.tbl_validate(message.content or message) 152 | end 153 | end 154 | 155 | --- Low level API 156 | -- @section low-level-api 157 | 158 | --- Validate an OSC message address. 159 | -- @tparam string addr The address to validate. 160 | function Message.address_validate(addr) 161 | assert(not addr:find('[%s#*,%[%]{}%?]'), 'Invalid characters found in address.') 162 | end 163 | 164 | --- Validate a table to be used as a message constructor. 165 | -- @tparam table tbl The table to validate. 166 | function Message.tbl_validate(tbl) 167 | assert(tbl.address, 'Missing "address" field.') 168 | Message.address_validate(tbl.address) 169 | assert(tbl.types, 'Missing "types" field.') 170 | assert(#tbl.types == #tbl, 'Types and arguments mismatch') 171 | end 172 | 173 | --- Validate a binary string to see if it is a valid OSC message. 174 | -- @tparam string bytes The byte string to validate. 175 | -- @tparam[opt] integer offset Byte offset. 176 | function Message.bytes_validate(bytes, offset) 177 | local value 178 | assert(#bytes % 4 == 0, 'OSC message data must be a multiple of 4.') 179 | value, offset = Types.unpack.s(bytes, offset) 180 | assert(value:sub(1, 1) == '/', 'Invalid OSC address.') 181 | value = Types.unpack.s(bytes, offset) 182 | assert(value:sub(1, 1) == ',', 'Error: malformed type tag.') 183 | end 184 | 185 | --- Pack a table to a byte string. 186 | -- 187 | -- The returned object is suitable for sending via a transport layer such as 188 | -- UDP or TCP. 189 | -- 190 | -- Call `Message.validate()` before passing arguments to this function to 191 | -- ensure that the table is suitable for packing. 192 | -- 193 | -- @param tbl The content to pack. 194 | -- @return OSC data packet (byte string). 195 | function Message.pack(tbl) 196 | local packet = {} 197 | local address = tbl.address 198 | local types = tbl.types 199 | -- types 200 | packet[#packet + 1] = Types.pack.s(address) 201 | packet[#packet + 1] = Types.pack.s(',' .. types) 202 | -- arguments 203 | local index = 1 204 | for type in types:gmatch('.') do 205 | local item = tbl[index] 206 | if item ~= nil then 207 | if Types.pack[type] then 208 | packet[#packet + 1] = Types.pack[type](item) 209 | end 210 | index = index + 1 211 | end 212 | end 213 | return table.concat(packet, '') 214 | end 215 | 216 | --- Unpack OSC message byte string. 217 | -- 218 | -- Call `Message.validate()` before passing arguments to this function to 219 | -- ensure that the table is suitable for unpacking. 220 | -- 221 | -- @param data The data to unpack. 222 | -- @param offset The initial offset into data. 223 | -- @return table with the content of the OSC message. 224 | function Message.unpack(data, offset) 225 | local value 226 | local message = {} 227 | -- address 228 | value, offset = Types.unpack.s(data, offset) 229 | message.address = value 230 | -- type tag 231 | value, offset = Types.unpack.s(data, offset) 232 | local types = value:sub(2) -- remove prefix 233 | message.types = types 234 | -- arguments 235 | for type in types:gmatch('.') do 236 | if Types.unpack[type] then 237 | value, offset = Types.unpack[type](data, offset) 238 | message[#message + 1] = value 239 | end 240 | end 241 | return message, offset 242 | end 243 | 244 | return Message 245 | -------------------------------------------------------------------------------- /src/losc/packet.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | ------------------------- 26 | -- OSC packet. 27 | -- 28 | -- The unit of transmission of OSC is an OSC Packet. 29 | -- An OSC packet is either a messages or a bundle. 30 | -- 31 | -- @module losc.packet 32 | -- @author David Granström 33 | -- @license MIT 34 | -- @copyright David Granström 2021 35 | 36 | local relpath = (...):gsub('%.[^%.]+$', '') 37 | local Message = require(relpath .. '.message') 38 | local Bundle = require(relpath .. '.bundle') 39 | local Types = require(relpath .. '.types') 40 | 41 | local Packet = {} 42 | 43 | --- Check if a packet is a bundle or a message. 44 | -- @tparam string|table packet The packet to check. 45 | -- @return True if packet is a bundle otherwise false. 46 | function Packet.is_bundle(packet) 47 | if type(packet) == 'string' then 48 | local value = Types.unpack.s(packet) 49 | return value == '#bundle' 50 | elseif type(packet) == 'table' then 51 | packet = packet.content or packet 52 | return type(packet.timetag) == 'table' 53 | end 54 | end 55 | 56 | --- Validate a packet. Can be a message or a bundle. 57 | -- @tparam string|table packet The packet to validate. 58 | function Packet.validate(packet) 59 | if Packet.is_bundle(packet) then 60 | Bundle.validate(packet) 61 | else 62 | Message.validate(packet) 63 | end 64 | end 65 | 66 | --- Pack a Bundle or Message. 67 | -- @param tbl The table to pack. 68 | -- @return OSC data packet (byte string). 69 | function Packet.pack(tbl) 70 | if Packet.is_bundle(tbl) then 71 | Bundle.validate(tbl) 72 | return Bundle.pack(tbl.content or tbl) 73 | else 74 | Message.validate(tbl) 75 | return Message.pack(tbl.content or tbl) 76 | end 77 | end 78 | 79 | --- Unpack an OSC packet. 80 | -- @tparam string data The data to unpack. 81 | -- @return table with the content of the OSC message (bundle or message). 82 | function Packet.unpack(data) 83 | if Packet.is_bundle(data) then 84 | Bundle.validate(data) 85 | return Bundle.unpack(data) 86 | else 87 | Message.validate(data) 88 | return Message.unpack(data) 89 | end 90 | end 91 | 92 | return Packet 93 | -------------------------------------------------------------------------------- /src/losc/pattern.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | -------------------------------------- 26 | -- OSC pattern matcher and dispatcher. 27 | -- 28 | -- @module losc.pattern 29 | -- @author David Granström 30 | -- @license MIT 31 | -- @copyright David Granström 2021 32 | 33 | local relpath = (...):gsub('%.[^%.]+$', '') 34 | local Packet = require(relpath .. '.packet') 35 | local Timetag = require(relpath .. '.timetag') 36 | 37 | local Pattern = {} 38 | 39 | local ts = Timetag.get_timestamp 40 | 41 | --- Escape magic lua characters from a pattern. 42 | -- @tparam string pattern The pattern to escape. 43 | -- @return A string with all magic lua characters escaped and OSC wildcards 44 | -- converted to lua pattern matching wildcards. 45 | function Pattern.escape(pattern) 46 | -- escape lua magic chars (order matters) 47 | pattern = pattern:gsub('%%', '%%%%') 48 | pattern = pattern:gsub('%.', '%%.') 49 | pattern = pattern:gsub('%(', '%%(') 50 | pattern = pattern:gsub('%)', '%%)') 51 | pattern = pattern:gsub('%+', '%%+') 52 | pattern = pattern:gsub('%$', '%%$') 53 | -- convert osc wildcards to lua patterns 54 | pattern = pattern:gsub('%*', '.*') 55 | pattern = pattern:gsub('%?', '.') 56 | pattern = pattern:gsub('%[!', '[^') 57 | pattern = pattern:gsub('%]', ']+') 58 | return pattern 59 | end 60 | 61 | local function match(key, address) 62 | local result = address:match(key) == address 63 | -- try and match group instead 64 | if not result and key:find('{') then 65 | local index = 1 66 | local tmps = '' 67 | for c in key:gmatch('.') do 68 | local a = address:sub(index, index) 69 | if a == c then 70 | tmps = tmps .. c 71 | index = index + 1 72 | end 73 | end 74 | result = tmps == address 75 | end 76 | return result 77 | end 78 | 79 | local function invoke(message, timestamp, plugin) 80 | local address = message.address 81 | local now = plugin:now():timestamp(plugin.precision) 82 | local ignore_late = plugin.options.ignore_late or false 83 | if ignore_late and timestamp > 0 and timestamp < now then 84 | return 85 | end 86 | if plugin.handlers then 87 | for _, handler in pairs(plugin.handlers) do 88 | if match(handler.pattern, address) then 89 | plugin:schedule(timestamp - now, function() 90 | handler.callback({ 91 | timestamp = now, 92 | message = message, 93 | remote_info = plugin.remote_info or {}, 94 | }) 95 | end) 96 | end 97 | end 98 | end 99 | end 100 | 101 | local function dispatch(packet, plugin) 102 | if Packet.is_bundle(packet) then 103 | for _, item in ipairs(packet) do 104 | if Packet.is_bundle(item) then 105 | if ts(item.timetag, plugin.precision) >= ts(packet.timetag, plugin.precision) then 106 | dispatch(item, plugin) 107 | else 108 | error('Bundle timestamp is older than timestamp of enclosing bundle') 109 | end 110 | else 111 | invoke(item, ts(packet.timetag, plugin.precision), plugin) 112 | end 113 | end 114 | else 115 | invoke(packet, 0, plugin) 116 | end 117 | end 118 | 119 | --- Dispatch OSC packets. 120 | -- @tparam string data Packed OSC data byte string. 121 | -- @tparam table plugin The plugin to dispatch the message through. 122 | function Pattern.dispatch(data, plugin) 123 | local packet = Packet.unpack(data) 124 | dispatch(packet, plugin) 125 | end 126 | 127 | return Pattern 128 | -------------------------------------------------------------------------------- /src/losc/plugins/udp-libuv.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | -------------------------------------------- 26 | -- UDP client/server implemented with libuv. 27 | -- 28 | -- These methods should be called from the main `losc` API. 29 | -- 30 | -- @module losc.plugins.udp-libuv 31 | -- @author David Granström 32 | -- @license MIT 33 | -- @copyright David Granström 2021 34 | 35 | local uv = require'luv' 36 | 37 | local relpath = (...):gsub('%.[^%.]+$', '') 38 | relpath = relpath:gsub('%.[^%.]+$', '') 39 | local Timetag = require(relpath .. '.timetag') 40 | local Pattern = require(relpath .. '.pattern') 41 | local Packet = require(relpath .. '.packet') 42 | 43 | local M = {} 44 | M.__index = M 45 | --- Fractional precision for bundle scheduling. 46 | -- 1000 is milliseconds. 1000000 is microseconds etc. Any precision is valid 47 | -- that makes sense for the plugin's scheduling function. 48 | M.precision = 1000 49 | 50 | --- Create a new instance. 51 | -- @tparam[options] table options Options. 52 | -- @usage local udp = plugin.new() 53 | -- @usage 54 | -- local udp = plugin.new { 55 | -- sendAddr = '127.0.0.1', 56 | -- sendPort = 9000, 57 | -- recvAddr = '127.0.0.1', 58 | -- recvPort = 8000, 59 | -- ignore_late = true, -- ignore late bundles 60 | -- } 61 | function M.new(options) 62 | local self = setmetatable({}, M) 63 | self.options = options or {} 64 | self.handle = uv.new_udp('inet') 65 | assert(self.handle, 'Could not create UDP handle.') 66 | return self 67 | end 68 | 69 | --- Create a Timetag with the current time. 70 | -- Precision is in milliseconds. 71 | -- @return Timetag object with current time. 72 | function M:now() -- luacheck: ignore 73 | local s, m = uv.gettimeofday() 74 | return Timetag.new(s, m / M.precision) 75 | end 76 | 77 | --- Schedule a OSC method for dispatch. 78 | -- 79 | -- @tparam number timestamp When to schedule the bundle. 80 | -- @tparam function handler The OSC handler to call. 81 | function M:schedule(timestamp, handler) -- luacheck: ignore 82 | timestamp = math.max(0, timestamp) 83 | if timestamp > 0 then 84 | local timer = uv.new_timer() 85 | timer:start(timestamp, 0, handler) 86 | else 87 | handler() 88 | end 89 | end 90 | 91 | --- Start UDP server. 92 | -- This function is blocking. 93 | -- @tparam string host IP address (e.g. '127.0.0.1'). 94 | -- @tparam number port The port to listen on. 95 | function M:open(host, port) 96 | host = host or self.options.recvAddr 97 | port = port or self.options.recvPort 98 | self.handle:bind(host, port, {reuseaddr=true}) 99 | self.handle:recv_start(function(err, data, addr) 100 | assert(not err, err) 101 | if data then 102 | self.remote_info = addr 103 | local ok, errormsg = pcall(Pattern.dispatch, data, self) 104 | if not ok then 105 | print(errormsg) 106 | end 107 | end 108 | end) 109 | -- updated if port 0 is passed in as default (chooses a random port) 110 | self.options.recvPort = self.handle:getsockname().port 111 | uv.run() 112 | end 113 | 114 | --- Close UDP server. 115 | function M:close() 116 | self.handle:recv_stop() 117 | if not self.handle:is_closing() then 118 | self.handle:close() 119 | end 120 | uv.walk(uv.close) 121 | end 122 | 123 | --- Send a OSC packet. 124 | -- @tparam table packet The packet to send. 125 | -- @tparam[opt] string address The IP address to send to. 126 | -- @tparam[opt] number port The port to send to. 127 | function M:send(packet, address, port) 128 | address = address or self.options.sendAddr 129 | port = port or self.options.sendPort 130 | packet = assert(Packet.pack(packet)) 131 | self.handle:try_send(packet, address, port) 132 | end 133 | 134 | return M 135 | -------------------------------------------------------------------------------- /src/losc/plugins/udp-socket.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | ------------------------------------------------- 26 | -- UDP client/server implemented using luasocket. 27 | -- 28 | -- These methods should be called from the main `losc` API. 29 | -- 30 | -- @module losc.plugins.udp-socket 31 | -- @author David Granström 32 | -- @license MIT 33 | -- @copyright David Granström 2021 34 | 35 | local socket = require'socket' 36 | 37 | local relpath = (...):gsub('%.[^%.]+$', '') 38 | relpath = relpath:gsub('%.[^%.]+$', '') 39 | local Timetag = require(relpath .. '.timetag') 40 | local Pattern = require(relpath .. '.pattern') 41 | local Packet = require(relpath .. '.packet') 42 | 43 | local M = {} 44 | M.__index = M 45 | --- Fractional precision for bundle scheduling. 46 | -- 1000 is milliseconds. 1000000 is microseconds etc. Any precision is valid 47 | -- that makes sense for the plugin's scheduling function. 48 | M.precision = 1000 49 | 50 | --- Create a new instance. 51 | -- @tparam[options] table options Options. 52 | -- @usage local udp = plugin.new() 53 | -- @usage 54 | -- local udp = plugin.new { 55 | -- sendAddr = '127.0.0.1', 56 | -- sendPort = 9000, 57 | -- recvAddr = '127.0.0.1', 58 | -- recvPort = 8000, 59 | -- ignore_late = true, -- ignore late bundles 60 | -- } 61 | function M.new(options) 62 | local self = setmetatable({}, M) 63 | self.options = options or {} 64 | self.handle = socket.udp() 65 | self.client_handle = socket.udp() 66 | assert(self.handle, 'Could not create UDP handle.') 67 | assert(self.client_handle, 'Could not create UDP handle.') 68 | return self 69 | end 70 | 71 | --- Create a Timetag with the current time. 72 | -- Precision is in milliseconds. 73 | -- @return Timetag object with current time. 74 | function M:now() -- luacheck: ignore 75 | local now = os.time() 76 | local millis = math.floor(((socket.gettime() - now) * 1000) + 0.5) 77 | return Timetag.new(now, millis) 78 | end 79 | 80 | --- Schedule a OSC method for dispatch. 81 | -- 82 | -- *This plugin does not support scheduled bundles - timestamps will be ignored.* 83 | -- @tparam number timestamp When to schedule the bundle. 84 | -- @tparam function handler The OSC handler to call. 85 | function M:schedule(timestamp, handler) -- luacheck: ignore 86 | handler() 87 | end 88 | 89 | --- Start UDP server. 90 | -- This function is blocking. 91 | -- @tparam string host IP address (e.g. 'localhost'). 92 | -- @tparam number port The port to listen on. 93 | function M:open(host, port) 94 | host = host or self.options.recvAddr 95 | host = socket.dns.toip(host) 96 | port = port or self.options.recvPort 97 | self.handle:setsockname(host, port) 98 | while true do 99 | local data = self.handle:receive() 100 | if data then 101 | local ok, err = pcall(Pattern.dispatch, data, self) 102 | if not ok then 103 | print(err) 104 | end 105 | end 106 | end 107 | end 108 | 109 | --- Close UDP server. 110 | function M:close() 111 | if self.handle then 112 | self.handle:close() 113 | end 114 | self.client_handle:close() 115 | end 116 | 117 | --- Send a OSC packet. 118 | -- @tparam table packet The packet to send. 119 | -- @tparam[opt] string address The IP address to send to. 120 | -- @tparam[opt] number port The port to send to. 121 | function M:send(packet, address, port) 122 | address = address or self.options.sendAddr 123 | address = socket.dns.toip(address) 124 | port = port or self.options.sendPort 125 | packet = assert(Packet.pack(packet)) 126 | self.client_handle:sendto(packet, address, port) 127 | end 128 | 129 | return M 130 | -------------------------------------------------------------------------------- /src/losc/serializer.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | ------------------------ 26 | -- Serializer functions. 27 | -- 28 | -- Lua >= 5.3 will use `string.pack`/`string.unpack` 29 | -- 30 | -- Lua < 5.3 and luajit will use `struct` if available, otherwise `lua-struct` (which is bundled as a fallback). 31 | -- 32 | -- @module losc.serializer 33 | -- @author David Granström 34 | -- @license MIT 35 | -- @copyright David Granström 2021 36 | 37 | local relpath = (...):gsub('%.[^%.]+$', '') 38 | local Serializer = {} 39 | 40 | --- Require a function for packing. 41 | -- @return A suitable packing function as explained in the header of this file. 42 | function Serializer.pack() 43 | if string.pack then 44 | return string.pack 45 | end 46 | local ok, _ = pcall(require, 'struct') 47 | if ok then 48 | return require'struct'.pack 49 | else 50 | return require(relpath .. '.lib.struct').pack 51 | end 52 | end 53 | 54 | --- Require a function for unpacking. 55 | -- @return A suitable unpacking function as explained in the header of this file. 56 | function Serializer.unpack() 57 | if string.unpack then 58 | return string.unpack 59 | end 60 | local ok, _ = pcall(require, 'struct') 61 | if ok then 62 | return require'struct'.unpack 63 | else 64 | return require(relpath .. '.lib.struct').unpack 65 | end 66 | end 67 | 68 | return Serializer 69 | -------------------------------------------------------------------------------- /src/losc/timetag.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | -------------- 26 | -- OSC Timetag. 27 | -- 28 | -- Time tags are represented by a 64 bit fixed point number. The first 32 bits 29 | -- specify the number of seconds since midnight on January 1, 1900, and the 30 | -- last 32 bits specify fractional parts of a second to a precision of about 31 | -- 200 picoseconds. This is the representation used by Internet NTP timestamps. 32 | -- 33 | -- @module losc.timetag 34 | -- @author David Granström 35 | -- @license MIT 36 | -- @copyright David Granström 2021 37 | 38 | local relpath = (...):gsub('%.[^%.]+$', '') 39 | local Serializer = require(relpath .. '.serializer') 40 | local _pack = Serializer.pack() 41 | local _unpack = Serializer.unpack() 42 | 43 | local Timetag = {} 44 | 45 | -- 70 years in seconds (1970 - 1900) 46 | local NTP_SEC_OFFSET = 2208988800 47 | -- 2^32 48 | local TWO_POW_32 = 4294967296 49 | 50 | local function tt_add(timetag, seconds) 51 | local sec = math.floor(seconds) 52 | local frac = math.floor(TWO_POW_32 * (seconds - sec) + 0.5) 53 | sec = sec + timetag.content.seconds 54 | frac = frac + timetag.content.fractions 55 | return Timetag.new_raw(sec, frac) 56 | end 57 | 58 | Timetag.__index = Timetag 59 | 60 | --- Add a time offset to a Timetag. 61 | -- This overloads the `+` operator for Timetags and should not be called directly. 62 | -- @usage local tt = Timetag.new(os.time()) + 0.25 63 | Timetag.__add = function(a, b) 64 | if type(a) == 'number' then 65 | return tt_add(b, a) 66 | end 67 | if type(b) == 'number' then 68 | return tt_add(a, b) 69 | end 70 | end 71 | 72 | --- Low level API 73 | -- @section low-level-api 74 | 75 | --- Create a new Timetag. 76 | -- 77 | -- @param[opt] tbl Table with timetag content. 78 | -- @param[opt] seconds Seconds since January 1st 1900 in the UTC timezone. 79 | -- @param[opt] fractions Fractions expressed as 1/2^32 of a second. 80 | -- 81 | -- If there are no arguments a timetag with special value of "immediate" will be returned. 82 | function Timetag.new_raw(...) 83 | local self = setmetatable({}, Timetag) 84 | local args = {...} 85 | -- 0x0000000000000001 equals "now", so this is the default. 86 | self.content = {seconds = 0, fractions = 1} 87 | if #args >= 1 then 88 | if type(args[1]) == 'table' then 89 | self.content = args[1] 90 | elseif type(args[1]) == 'number' and not args[2] then 91 | self.content.seconds = args[1] 92 | elseif type(args[1]) == 'number' and type(args[2]) == 'number' then 93 | self.content.seconds = args[1] 94 | self.content.fractions = args[2] 95 | end 96 | end 97 | return self 98 | end 99 | 100 | --- High level API 101 | -- @section high-level-api 102 | 103 | --- New using a seconds and fractions. 104 | -- 105 | -- Given nil arguments will return a timetag with special value "immediate". 106 | -- 107 | -- @tparam[opt] integer seconds Timestamp seconds. 108 | -- @tparam[opt] integer fractions Timestamp fractions. 109 | -- @tparam[opt] integer precision The fraction precision. default 1000 (`milliseconds`) 110 | -- @usage local tt = Timetag.new() -- immediate 111 | -- @usage local tt = Timetag.new(os.time()) 112 | -- @usage local tt = Timetag.new(tv.sec, tv.usec, 1e6) 113 | -- @see Timetag.new_raw 114 | function Timetag.new(seconds, fractions, precision) 115 | precision = precision or 1000 116 | if not seconds and not fractions then 117 | return Timetag.new_raw() 118 | end 119 | local secs, frac 120 | secs = (seconds or 0) + NTP_SEC_OFFSET 121 | frac = math.floor((fractions or 0) * (TWO_POW_32 / precision) + 0.5) 122 | return Timetag.new_raw(secs, frac) 123 | end 124 | 125 | --- Create a new OSC Timetag from a timestamp. 126 | -- 127 | -- @param time The timestamp to use. 128 | -- @param[opt] precision The fraction precision. default 1000 129 | -- @return A Timetag object. 130 | -- @usage local tt = Timetag.new_from_timestamp(time) 131 | function Timetag.new_from_timestamp(time, precision) 132 | precision = precision or 1000 133 | local seconds = math.floor(time / precision) 134 | local fracs = math.floor(precision * (time / precision - seconds) + 0.5) 135 | return Timetag.new(seconds, fracs) 136 | end 137 | 138 | --- Get a timestamp value with arbitrary precision. 139 | -- @param precision The precision to use. default 1000 (`milliseconds`) 140 | -- @return Timestamp value. 141 | -- @usage 142 | -- local tt = Timetag.new(os.time(), 500) 143 | -- local timestamp = tt:timestamp() 144 | function Timetag:timestamp(precision) 145 | return Timetag.get_timestamp(self.content, precision) 146 | end 147 | 148 | --- Get seconds. 149 | -- @return Timetag seconds. 150 | function Timetag:seconds() 151 | return self.content.seconds 152 | end 153 | 154 | --- Get fractions. 155 | -- @return Timetag fractions. 156 | function Timetag:fractions() 157 | return self.content.fractions 158 | end 159 | 160 | --- Low level API 161 | -- @section low-level-api 162 | 163 | --- Get a timestamp with arbitrary precision. 164 | -- @param tbl Table with seconds and fractions. 165 | -- @param[opt] precision The fraction precision. default 1000 166 | -- @return Timetag value. 167 | function Timetag.get_timestamp(tbl, precision) 168 | precision = precision or 1000 169 | local seconds = precision * math.max(0, tbl.seconds - NTP_SEC_OFFSET) 170 | local fractions = math.floor(precision * (tbl.fractions / TWO_POW_32) + 0.5) 171 | return seconds + fractions 172 | end 173 | 174 | --- Pack an OSC Timetag. 175 | -- 176 | -- The returned object is suitable for sending via a transport layer such as 177 | -- UDP or TCP. 178 | -- 179 | -- @tparam table tbl The timetag to pack. 180 | -- @return OSC data packet (byte string). 181 | -- @usage 182 | -- local tt = {seconds = os.time(), fractions = 0} 183 | -- local data = Timetag.pack(tt) 184 | function Timetag.pack(tbl) 185 | local data = {} 186 | data[#data + 1] = _pack('>I4', tbl.seconds) 187 | data[#data + 1] = _pack('>I4', tbl.fractions) 188 | return table.concat(data, '') 189 | end 190 | 191 | --- Unpack an OSC Timetag. 192 | -- 193 | -- @param data The data to unpack. 194 | -- @param offset The initial offset into data. 195 | -- @return First is a table with seconds and fractions, second is index of the bytes read + 1. 196 | function Timetag.unpack(data, offset) 197 | local seconds, fractions 198 | seconds, offset = _unpack('>I4', data, offset) 199 | fractions, offset = _unpack('>I4', data, offset) 200 | return {seconds = seconds, fractions = fractions}, offset 201 | end 202 | 203 | return Timetag 204 | -------------------------------------------------------------------------------- /src/losc/types.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | MIT License 3 | 4 | Copyright (c) 2021 David Granström 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | ]] 24 | 25 | ------------- 26 | -- OSC Types. 27 | -- 28 | -- The size of every atomic data type in OSC is a multiple of 32 bits. 29 | -- 30 | -- @module losc.types 31 | -- @author David Granström 32 | -- @license MIT 33 | -- @copyright David Granström 2021 34 | 35 | local relpath = (...):gsub('%.[^%.]+$', '') 36 | local Serializer = require(relpath .. '.serializer') 37 | local Timetag = require(relpath .. '.timetag') 38 | 39 | local _pack = Serializer.pack() 40 | local _unpack = Serializer.unpack() 41 | local has_string_pack = string.pack and true or false 42 | 43 | local Types = {} 44 | 45 | --- Type pack functions. 46 | -- 47 | -- Custom pack functions can be added to this table and standard functions can 48 | -- be re-assigned if necessary. 49 | -- 50 | -- This table can be called to pack a value in protected mode (pcall). 51 | -- @usage local ok, data = Types.pack('s', 'hello') 52 | -- if ok then 53 | -- -- do something with data. 54 | -- end 55 | Types.pack = {} 56 | setmetatable(Types.pack, { 57 | __call = function(self, type, value) 58 | return pcall(self[type], value) 59 | end 60 | }) 61 | 62 | --- Type unpack functions. 63 | -- 64 | -- Custom unpack functions can be added to this table and standard functions 65 | -- can be re-assigned if necessary. 66 | -- 67 | -- This table can be called to unpack a value in protected mode (pcall). 68 | -- @usage local ok, value, index = Types.unpack('s', data, 1) 69 | -- if ok then 70 | -- -- do something with value/index. 71 | -- end 72 | Types.unpack = {} 73 | setmetatable(Types.unpack, { 74 | __call = function(self, type, data, offset) 75 | return pcall(self[type], data, offset) 76 | end 77 | }) 78 | 79 | --- Get available types. 80 | -- @tparam table tbl `Types.unpack` or `Types.pack` 81 | -- @return Table with available types. 82 | -- @usage local types = Types.types(Types.pack) 83 | -- @usage local types = Types.types(Types.unpack) 84 | function Types.get(tbl) 85 | local types = {} 86 | for k, _ in pairs(tbl) do 87 | types[#types + 1] = k 88 | end 89 | return types 90 | end 91 | 92 | local function strsize(s) 93 | return 4 * (math.floor(#s / 4) + 1) 94 | end 95 | 96 | local function blobsize(b) 97 | return 4 * (math.floor((#b + 3) / 4)) 98 | end 99 | 100 | --- Atomic types. 101 | -- @section atomic-types 102 | 103 | --- 32-bit big-endian two's complement integer. 104 | -- @param value The value to pack. 105 | -- @return Binary string buffer. 106 | Types.pack.i = function(value) 107 | return _pack('>i4', value) 108 | end 109 | 110 | --- 32-bit big-endian two's complement integer. 111 | -- @param data The data to unpack. 112 | -- @param[opt] offset Initial offset into data. 113 | -- @return value, index of the bytes read + 1. 114 | Types.unpack.i = function(data, offset) 115 | return _unpack('>i4', data, offset) 116 | end 117 | 118 | --- 32-bit big-endian IEEE 754 floating point number. 119 | -- @param value The value to pack. 120 | -- @return Binary string buffer. 121 | Types.pack.f = function(value) 122 | return _pack('>f', value) 123 | end 124 | 125 | --- 32-bit big-endian IEEE 754 floating point number. 126 | -- @param data The data to unpack. 127 | -- @param[opt] offset Initial offset into data. 128 | -- @return value, index of the bytes read + 1. 129 | Types.unpack.f = function(data, offset) 130 | return _unpack('>f', data, offset) 131 | end 132 | 133 | --- String (null terminated) 134 | -- @param value The value to pack. 135 | -- @return Binary string buffer. 136 | Types.pack.s = function(value) 137 | local len = strsize(value) 138 | local fmt = 'c' .. len 139 | value = value .. string.rep(string.char(0), len - #value) 140 | return _pack('>' .. fmt, value) 141 | end 142 | 143 | --- String (null terminated) 144 | -- @param data The data to unpack. 145 | -- @param[opt] offset Initial offset into data. 146 | -- @return value, index of the bytes read + 1. 147 | Types.unpack.s = function(data, offset) 148 | local fmt = has_string_pack and 'z' or 's' 149 | local str = _unpack('>' .. fmt, data, offset) 150 | return str, strsize(str) + (offset or 1) 151 | end 152 | 153 | --- Blob (arbitrary binary data) 154 | -- @param value The value to pack. 155 | -- @return Binary string buffer. 156 | Types.pack.b = function(value) 157 | local size = #value 158 | local aligned = blobsize(value) 159 | local fmt = 'c' .. aligned 160 | value = value .. string.rep(string.char(0), aligned - size) 161 | return _pack('>I4' .. fmt, size, value) 162 | end 163 | 164 | --- Blob (arbitrary binary data) 165 | -- @param data The data to unpack. 166 | -- @param[opt] offset Initial offset into data. 167 | -- @return value, index of the bytes read + 1. 168 | Types.unpack.b = function(data, offset) 169 | local size, blob 170 | size, offset = _unpack('>I4', data, offset) 171 | blob, offset = _unpack('>c' .. size, data, offset) 172 | return blob, offset + blobsize(blob) - size 173 | end 174 | 175 | --- Extended types. 176 | -- @section extended-types 177 | 178 | if has_string_pack then 179 | --- 64 bit big-endian two's complement integer. 180 | -- 181 | -- **NOTE** This type is only supported for lua >= 5.3. 182 | -- @param value The value to pack. 183 | -- @return Binary string buffer. 184 | Types.pack.h = function(value) 185 | return _pack('>i8', value) 186 | end 187 | end 188 | 189 | if has_string_pack then 190 | --- 64 bit big-endian two's complement integer. 191 | -- 192 | -- **NOTE** This type is only supported for lua >= 5.3. 193 | -- @param data The data to unpack. 194 | -- @param[opt] offset Initial offset into data. 195 | -- @return value, index of the bytes read + 1. 196 | Types.unpack.h = function(data, offset) 197 | return _unpack('>i8', data, offset) 198 | end 199 | end 200 | 201 | --- Timetag (64-bit integer divided into upper and lower part) 202 | -- @param value Table with seconds and fractions. 203 | -- @return Binary string buffer. 204 | -- @see losc.timetag 205 | Types.pack.t = function(value) 206 | return Timetag.pack(value) 207 | end 208 | 209 | --- Timetag (64-bit integer divided into upper and lower part) 210 | -- @param data The data to unpack. 211 | -- @param[opt] offset Initial offset into data. 212 | -- @return value, index of the bytes read + 1. 213 | -- @see losc.timetag 214 | Types.unpack.t = function(data, offset) 215 | return Timetag.unpack(data, offset) 216 | end 217 | 218 | --- 64-bit big-endian IEEE 754 floating point number. 219 | -- @param value The value to pack. 220 | -- @return Binary string buffer. 221 | Types.pack.d = function(value) 222 | return _pack('>d', value) 223 | end 224 | 225 | --- 64-bit big-endian IEEE 754 floating point number. 226 | -- @param data The data to unpack. 227 | -- @param[opt] offset Initial offset into data. 228 | -- @return value, index of the bytes read + 1. 229 | Types.unpack.d = function(data, offset) 230 | return _unpack('>d', data, offset) 231 | end 232 | 233 | --- Boolean true. 234 | -- This type does not have a corresponding `pack` method. 235 | -- @param _ Not used. 236 | -- @param[opt] offset Initial offset into data. 237 | -- @return true (boolean) and byte offset (not incremented). 238 | Types.unpack.T = function(_, offset) 239 | return true, offset or 0 240 | end 241 | 242 | --- Boolean false. 243 | -- This type does not have a corresponding `pack` method. 244 | -- @param _ Not used. 245 | -- @param[opt] offset Initial offset into data. 246 | -- @return false (boolean) and byte offset (not incremented). 247 | Types.unpack.F = function(_, offset) 248 | return false, offset or 0 249 | end 250 | 251 | --- Nil. 252 | -- This type does not have a corresponding `pack` method. 253 | -- @param _ Not used. 254 | -- @param[opt] offset Initial offset into data. 255 | -- @return false (since nil cannot be represented in a lua table) and byte offset (not incremented). 256 | Types.unpack.N = function(_, offset) 257 | -- TODO: decide on what to return here.. 258 | return false, offset or 0 259 | end 260 | 261 | --- Infinitum. 262 | -- This type does not have a corresponding `pack` method. 263 | -- @param _ Not used. 264 | -- @param[opt] offset Initial offset into data. 265 | -- @return math.huge and byte offset (not incremented). 266 | Types.unpack.I = function(_, offset) 267 | return math.huge, offset or 0 268 | end 269 | 270 | return Types 271 | --------------------------------------------------------------------------------