Simple OSC functions for LÖVE game engine

Programming applications for making music on Linux.

Moderators: MattKingUSA, khz

Post Reply
GMoon
Established Member
Posts: 51
Joined: Sat Oct 03, 2020 1:10 pm
Has thanked: 11 times
Been thanked: 20 times

Simple OSC functions for LÖVE game engine

Post by GMoon »

Here's a set of simple functions (not even a library) to package OSC data (sent via the socket layer), for the LÖVE game engine.
https://love2d.org/

Why? LÖVE is supported on many platforms, so it works on the Linux desktop and mobile devices (and other OS's), it's interpreted (code is easy to modify) and if LÖVE runs on your platform, there are no other dependencies (other than GUI libraries, if that's desired).

Plus any other options for OSC controllers (other than TouchOSC or MobMuPlat, etc.) are a good thing, IMHO.

NOTE: there are several lua OSC libraries, but none seem to function in LÖVE 11.3.

The OSC functions:

oscLv.lua

Code: Select all

-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
-- "oscLv" by Doug Garmon, 2021
-- MINIMAL OSC packing implementation for love2d
-- 	License: "The Unlicense"
-- 	For more information, please refer to <https://unlicense.org/>
-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
-- osc packet functions for sockets
local endpad = string.char(0, 0, 0, 0)
local modtab = {0, 3, 2, 1}
function oscString (Str)
local newS, mod
	newS = Str..string.char(0x0)
	mod = string.len(newS) % 4
return(newS..string.sub(endpad, 1, modtab[mod + 1]))
end

function oscType (Str)
return(oscString(','..Str))
end

function oscSymbol (Str)
local s1, s2 = string.find(Str, " ")
return(oscString(string.sub(Str, 1, s1)))
end

function oscPacket (addrS, typeS, msgTab)
local strl, types
	strl = oscString(addrS)..oscType(typeS)

	for argC = 1, #msgTab do
	types = string.sub(typeS, argC, argC)
		if types == 's' then 
			strl = strl..oscString(msgTab[argC])
		elseif types == 'S' then
			strl = strl..oscSymbol(msgTab[argC])
		elseif types == 'f' then
			strl = strl..love.data.pack('string', '>f', msgTab[argC])
		elseif types == 'i' then
			strl = strl..love.data.pack('string', '>i', msgTab[argC])
		end
	end
return(strl)
end
-- osc packet functions END
-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
The main.lua file, with the love2d code and the socket lib code:

main.lua

Code: Select all

-- "oscLv" demo mail.lua file by Doug Garmon
-- 	License: "The Unlicense"
-- 	For more information, please refer to <https://unlicense.org/>
-- Networking code: from love2d.org -- Tutorial:Networking with UDP, client
local socket = require "socket"
local oscLv = require "oscLv"

-- the address and port of the server
local address, port = "224.0.0.1", 20331
local mouse = {0, 0}
local mscale = 596
local lineloc = 400
local font = {}
-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
function math.clamp(low, n, high) return math.min(math.max(n, low), high) end

function love.load()
	love.window.setMode(601, 906, {resizable=false})
	font = love.graphics.newFont(16)
	love.graphics.setFont(font)
	
	udp = socket.udp()
	udp:settimeout(0)
	udp:setpeername(address, port)
end

function love.update(deltatime)

	if love.mouse.isDown(1) then
		mouse.x, mouse.y = love.mouse.getPosition()
		
		if lastX ~= mouse.x then
			local msgT = {2, math.clamp(0, mouse.x / mscale, 1)}
			local packet = oscPacket('/P2Jcli/0/pp', 'ff', msgT)
			udp:send(packet)
			
			lastX = mouse.x
		end
		if mouse.y > lineloc then
			love.event.quit()
		end
	end
end

function love.draw()
	love.graphics.setColor(0.5, 0.5, 0.5)
	love.graphics.line(10, lineloc, 590, lineloc )
	love.graphics.print("Click below line to quit", 100, lineloc + 10)
end
The socket module is included with LÖVE, so it's not an external dependency.

Create a folder, save the first code block as "oscLv.lua", then save the second code block as "main.lua".

CD to the parent dir, then type:
love <folder name>

The contents of the folder (two files) can be archived as a zip, then renamed to something like "demo.love". A double-click in Linux will load love2d and run the demo. A ".love" file is much easier to use on mobile devices, too.

Caveats:
- There's really no error checking (wouldn't call this a library, it's just a proof-of-concept).
- No blobs currently (just f,i,s,S types).
- Symbols are loosely documented (it seems to work). A symbol is truncated at the first space, though.
- No receive functions yet.

This demo just interprets mouse movements above the line, and outputs an OSC message. Clicking below the line quits the app.

I've tested this on the Ubuntu desktop, and an Android tablet. The screen orientation and size are set for portrait for my mobile device - that's easy to change. it's sending to the multicast group on port 20331. I've made some demos with GUI libraries, but his minimal demo is only about 2K of code (less if it's zipped).

I use this to receive and debug the OSC messages:
oscdump 20331

(oscdump is part of the liblo OSC library) Also tested with pd2jack.

USE:

Create a table with the data:

local msgT = {float1, float2)}

Make the packet - Format: (/OSCADDR, 'theDataTypes', the data table) :

local packet = oscPacket('/P2Jcli/0/pp', 'ff', msgT)

Send the packet:

udp:send(packet)
Last edited by GMoon on Wed Oct 13, 2021 5:55 pm, edited 1 time in total.
Basslint
Established Member
Posts: 1511
Joined: Sun Jan 27, 2019 2:25 pm
Location: Italy
Has thanked: 382 times
Been thanked: 298 times

Re: Simple OSC (send) functions for LÖVE game engine

Post by Basslint »

This is very cool! I used LÖVE some years ago but never really got into it. It's a good engine to quickly built stuff and I am sure that this library can help people make cool controllers and games :D
The community of believers was of one heart and mind, and no one claimed that any of his possessions was his own, but they had everything in common. [Acts 4:32]

Please donate time (even bug reports) or money to libre software 🎁

Jam on openSUSE + GeekosDAW!
GMoon
Established Member
Posts: 51
Joined: Sat Oct 03, 2020 1:10 pm
Has thanked: 11 times
Been thanked: 20 times

Re: Simple OSC (send) functions for LÖVE game engine

Post by GMoon »

Basslint wrote: Sun Oct 10, 2021 6:21 am This is very cool! I used LÖVE some years ago but never really got into it. It's a good engine to quickly built stuff and I am sure that this library can help people make cool controllers and games :D
Thx!

I've made some progress with it -- I've got OSC receive functions working now, and decided to write a small GUI (I like knobs for controllers, and ironically that's just now being implemented). It all works from the desktop or mobile devices.

About half of the widgets in this screen grab are just for demo purposes, but the rest sends/receives OSC data correctly:
(Yeah, the knobs I just started on today :? , so they don't do anything but follow a mouse)
milv_gui1_sml.png
milv_gui1_sml.png (63.64 KiB) Viewed 11764 times
As mentioned in the first post it's a 'thin' OSC implementation (f,i,s,S) as that's all I need for Pure Data. But that's just a start.

I'll make a github pg soon (the gui is alpha right now, for sure). Lua and LÖVE make this (and the cross-platform stuff) pretty easy.
GMoon
Established Member
Posts: 51
Joined: Sat Oct 03, 2020 1:10 pm
Has thanked: 11 times
Been thanked: 20 times

Re: Simple OSC (send) functions for LÖVE game engine

Post by GMoon »

I'm remiss in not posting the OSC lua/love code. This includes functions for both sending and receiving OSC data, and works with mobile devices.

These are the OSC functions only. I'm sure these can be improved, but they do work...

I call this file "oscLv.lua", and it should be used with a "require" statement in your main.lua file.
local oscLv = require "oscLv"

Code: Select all

-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
-- "oscLv" by Doug Garmon, 2021
-- MINIMAL OSC packing/unpacking & send/receive implementation for love2d
-- 	License: "The Unlicense"
-- 	For more information, please refer to <https://unlicense.org/>
do
local socket = require "socket"
local mtab = {0, 3, 2, 1}
-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
-- osc client functions START
local cudp
function cliSetup(caddr, cport)
	cudp = socket.udp()
	cudp:settimeout(0)
	-- the address and port of the client
	cudp:setpeername(caddr, cport)
end

function sendOSC(pack)
	if cudp ~= nil then
		cudp:send(pack)
	end
end
-- osc packet functions for sockets
local endpad = string.char(0, 0, 0, 0)
function oscString (Str)
local newS, mod
	newS = Str..string.char(0x0)
	mod = string.len(newS) % 4
return(newS..string.sub(endpad, 1, mtab[mod + 1]))
end

function oscType (Str)
return(oscString(','..Str))
end

function oscSymbol (Str)
local s1, s2 = string.find(Str, " ")
return(oscString(string.sub(Str, 1, s1)))
end

function oscPacket (addrS, typeS, msgTab)
local strl, types
	strl = oscString(addrS)..oscType(typeS)
	for argC = 1, #msgTab do
	types = string.sub(typeS, argC, argC)
		if types == 's' then 
			strl = strl..oscString(msgTab[argC])
		elseif types == 'S' then
			strl = strl..oscSymbol(msgTab[argC])
		elseif types == 'f' then
			strl = strl..love.data.pack('string', '>f', msgTab[argC])
		elseif types == 'i' then
			strl = strl..love.data.pack('string', '>i', msgTab[argC])
		end
	end
return(strl)
end
-- osc client functions END
-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
-- osc server functions START
local rudp
-- start a server on port
function servSetup(raddr, rport)
	rudp = socket.udp()
	-- Don't block, setting the 'timeout' to zero
	rudp:settimeout(0)
	rudp:setsockname(raddr, rport)
end
-- poll the receiving UDP port (nonblocking)
function oscPoll()
local data, msg_or_ip, port_or_nil
	data, msg_or_ip, port_or_nil = rudp:receivefrom()
	if data == nil then
		if msg_or_ip ~= 'timeout' then
			error("Unknown network error: "..tostring(msg))
		end
	end
return data
end
-- unpack UDP OSC msg packet into:
--	oscAddr = oA
--	oscType = oT
--	oscData = oD
--	oscID = oI
--	oscCmd = oC
function oscUnpack(udpM)
local dTbl
local oA , oT, oD, oI, oC
	oA , oT, oD = udpM:match("^(/.+)%z+,(%a+)%z+(.+)")		
	oI = udpM:match("/(%d+)/")		
	oC = udpM:match("/(%w+)%z")		
	return oA, oT, oD, oI, oC
end
-- unpack OSC data block
function oscDataUnpack(oT, oD)
local tc, iv, nx, zloc
dTbl = {}
	for i = 1, #oT do
		tc = oT:sub(i,i)
		if tc == 'f' then
			iv, nx = love.data.unpack(">f", oD)
			oD = string.sub(oD, 5)
			table.insert(dTbl, tonumber(iv))
		elseif tc == 's' or tc == 'S' then
			zloc, nx = string.find(oD, '\0')
			local tmpS = string.sub(oD, 1, zloc - 1)
			iv = string.format("%s", tmpS)
			nx = zloc + mtab[zloc % 4 + 1]
			oD = string.sub(oD, nx + 1)
			table.insert(dTbl, tostring(iv))
		elseif tc == 'i' then
			iv, nx = love.data.unpack(">i", oD)
			oD = string.sub(oD, 5)
			table.insert(dTbl, tonumber(iv))
		end
	end
	return dTbl
end
-- osc/udp server functions END.
-- ++++++++++++++++++++++++++++++++++++++++++++++++++++
end
(REALLY) Simple docs:

Initialize the OSC sockets (usually in the love.load() callback). You recognize the IP addr and port (with mobile devices the multicast addr has generally been required):

Code: Select all

	-- setup send (client)
	cliSetup("224.0.0.1", 20331)	
	-- and receive (server)
	servSetup("224.0.0.1", 20341)
Sending a packet:

Code: Select all

	local msgT = {"@dsp", state}
	local packet = oscPacket('/P2Jcli/0/cmd', 'sf', msgT)
	sendOSC(packet)
Receiving
For simplicity's sake, the "server" is just a non-blocking poll of the socket. Define a function:

Code: Select all

-- poll the OSC server 
function myServ(slst)
	local packet = oscPoll()
	if packet then
		local oscADDR , oscTYPE, oscDATA, oscID, oscCMD = oscUnpack(packet)
		local dataT = oscDataUnpack(oscTYPE, oscDATA)
		
		-- we are cheating & only using the "cmd" part of the address that's extracted from the packet
		if oscCMD == 'pp' then
			setValue(slst, dataT[1], dataT[2])
		end
	end
end
Then call myServ() in the love.update() callback. It's not a separate thread, but love.update() is usually called 50 or 60 times/second. It works OK here :wink:

The above polling function is passed a list of widgets, and sets their value based on the incoming OSC data. Define your own function...

oscPoll() returns a UDP packet.

The oscUnpack(packet) returns these data structures:

oscADDR = OSC address
oscTYPE = the OSC type string
oscDATA = the data
oscID = this is custom -- I embed an ID# in my OSC addresses
oscCMD = The last part of the OSC address, after the final forward slash: for "/myadd/3/cmd" this returns 'cmd'

The oscDataUnpack(oscTYPE, oscDATA) function returns a table with each TYPE atom in an indexed table.

As noted in the first post, these functions only work with float, int, string, and symbol OSC data types, not all the types. Sorry.
Basslint
Established Member
Posts: 1511
Joined: Sun Jan 27, 2019 2:25 pm
Location: Italy
Has thanked: 382 times
Been thanked: 298 times

Re: Simple OSC functions for LÖVE game engine

Post by Basslint »

Please open a repo ASAP, posting code in forums is very common in the LÖVE community, I understand, but not very optimal :D
The community of believers was of one heart and mind, and no one claimed that any of his possessions was his own, but they had everything in common. [Acts 4:32]

Please donate time (even bug reports) or money to libre software 🎁

Jam on openSUSE + GeekosDAW!
GMoon
Established Member
Posts: 51
Joined: Sat Oct 03, 2020 1:10 pm
Has thanked: 11 times
Been thanked: 20 times

Re: Simple OSC functions for LÖVE game engine

Post by GMoon »

Basslint wrote: Thu Oct 14, 2021 5:54 pm Please open a repo ASAP, posting code in forums is very common in the LÖVE community, I understand, but not very optimal :D
Yep! Although I've only been messing with lua/LÖVE for 4-5 weeks, so I'm not sure what's customary in that community.

I posted the OSC code since it might be useful, and wasn't planning on making a repo until I was happy with the GUI aspect of my controller project. That, and the fact that the OSC code is smaller than some forum posts here :D .
Post Reply