Hey there ! I’m currently experimenting with bridging Renoise with Tidal Cycles. The goal is to do the sequencing with Tidal and the sound generation with Renoise.
I’ve tried two approaches: make Tidal send OSC messages that Renoise can understand out of the box, or make Renoise understand the messages Tidal sends to the regular audio backend.
The first approach works quite well, but introduces some warts in Tidal usage.
The second approach looks promising, but I have some timing issues. Basically I tried this in the TestPad:
- tidal sends a message to port 57120
- my “redirt” script listens on 57120 and receive the message
- the message is analysed,
(instrument, track, note, velocity)
are extracted, and a/renoise/trigger/note_on
message is sent back to renoise on port 8000 (note triggering is not exposed to the lua API).
Now, when I do this the timing sucks. Like very very much. And I have questions:
- Is it unavoidable?
- Is it because the TestPad does not have some form of optimization?
- My OSC messages have timetags, so you can send a message in advance and have the target schedule it exactly at the right moment. Does renoise do that?
Here is my proof of concept, for reference.
local OscMessage = renoise.Osc.Message
local OscBundle = renoise.Osc.Bundle
-- open a socket connection to the server
local server, socket_error = renoise.Socket.create_server(
"localhost", 57120, renoise.Socket.PROTOCOL_UDP)
if (socket_error) then
renoise.app():show_warning(("Failed to start the " ..
"OSC server. Error: '%s'"):format(socket_error))
return
end
-- open a socket connection to the client
local client, socket_error = renoise.Socket.create_client(
"localhost", 8000,
renoise.Socket.PROTOCOL_UDP)
if (socket_error) then
renoise.app():show_warning(("Failed to open the osc loopback with error: '%s'"):format(socket_error))
return
end
function linlin(input, srclo, srchi, dstlo, dsthi)
-- clamp input
if input < srclo then input = srclo end
if input > srchi then input = srchi end
-- normalize over [0,1]
local norm = (input - srclo) / srchi
-- transpose to [dstlo,dsthi]
return dstlo + norm * (dsthi-dstlo)
end
-- parse a tidal bundle and return a list of messages
function parseTidal(bundle)
local msgs = {}
for _,element in ipairs(bundle.elements) do
if element.pattern == "/play2" then
local msg = {}
local name = ""
for i,argument in ipairs(element.arguments) do
if i%2 == 1 then
name = argument.value
else
msg[name] = argument.value
end
end
msgs[#msgs+1] = msg
end
end
return msgs
end
function runTidal(msg)
-- rprint(msg)
-- track number
local track = msg.orbit+1
-- should we trigger a sound ?
if msg.s then
-- select instrument by number (or the first one if something is wrong)
local instr = (tonumber(msg.s) or 0) + 1
-- default octave is 4
local octave = msg.octave or 4
local note = octave * 12 + msg.note
-- rescale velocity from [0,1] to [0,127]
local velocity = linlin(msg.gain or 1 , 0, 1, 0, 127)
client:send(OscMessage("/renoise/trigger/note_on", {
{tag="i", value=instr},
{tag="i", value=track},
{tag="i", value=note},
{tag="i", value=velocity}
}))
end
end
server:run {
socket_message = function(socket, data)
-- decode the data to Osc
local mob, osc_error = renoise.Osc.from_binary_data(data)
-- show what we've got
if (mob) then
local msgs = parseTidal(mob)
for _,msg in ipairs(msgs) do
runTidal(msg)
end
else
print(("Got invalid OSC data, or data which is not " ..
"OSC data at all. Error: '%s'"):format(osc_error))
end
end
}