I made an example with a syntax I really like. It give some overhead, for sure, but with the benefit of constructing a synth patch in a single expression that is easily readable.
The idea is to render the top node in a nested tree (composite pattern) with a function like synth_patch:render(sample_index). Oversampling can be done by rendering fractions of sample positions. Pitching sub-nodes becomes a matter of transforming the sample position in that sub node.
Example:
local synth_patch = ADD {
MUL {
ADD {
note = 42,
sawtooth,
},
0.6,
adsr(0.05, 0.2, 0.6, 0.4, 1.0)
},
MUL {
noise,
0.15,
adsr(0.0, 0.05, 0.3, 0.2, 0.8)
}
}
local buffer = renoise.song().selected_sample.sample_buffer
for i = sample_pos, buffer.number_of_frames do
buffer:set_sample_data(1, sample_pos, synth_patch:render(sample_pos))
end
Any ideas, feedback or pointing out problems is appreciated. Is this the best general approach? 
Here is some dirty code of the full implementation if someone wanna try it out. Nothing fancy like filters/delays are added yet, but it would be easy to add those as factory functions. The code is a bit verbose due to microoptimizations.
Summary
SAMPLE_RATE = 44100
SAMPLE_RATE_INV = 1/SAMPLE_RATE
TWO_PI = math.pi * 2
FREQUENCY = 440
-- Localized math functions for faster lookup.
local sin = math.sin
local abs = math.abs
local random = math.random
class 'ADD' (Renderer)
function ADD:__init(args)
Renderer.__init(self, args)
self.render = function(obj, pos)
pos = pos * obj.base_scaling
local args = obj.args
local rslt = 0
for k = 1, #args do
local value = args[k]
local value_type = type(value)
if value_type == "function" then
value = value(pos)
elseif (not (value_type == "number")) and
value.render then
value = value:render(pos)
end
rslt = rslt + value
end
return rslt
end
end
class 'SUB' (Renderer)
function SUB:__init(args)
Renderer.__init(self, args)
self.type = Renderer.RENDER_TYPE_SUB
self.render = function(obj, pos)
pos = pos * obj.base_scaling
local args = obj.args
local rslt = 0
for k = 1, #args do
local value = args[k]
local value_type = type(value)
if value_type == "function" then
value = value(pos)
elseif (not (value_type == "number")) and
value.render then
value = value:render(pos)
end
if k == 1 then
rslt = rslt + value
else
rslt = rslt - value
end
end
return rslt
end
end
class 'MUL' (Renderer)
function MUL:__init(args)
Renderer.__init(self, args)
self.render = function(obj, pos)
pos = pos * obj.base_scaling
local args = obj.args
local rslt = 1
for k = 1, #args do
local value = args[k]
local value_type = type(value)
if value_type == "function" then
value = value(pos)
elseif (not (value_type == "number")) and
value.render then
value = value:render(pos)
end
rslt = rslt * value
end
return rslt
end
end
class 'DIV' (Renderer)
function DIV:__init(args)
Renderer.__init(self, args)
self.render = function(obj, pos)
pos = pos * obj.base_scaling
local args = obj.args
local rslt = 0
for k = 1, #args do
local value = args[k]
local value_type = type(value)
if value_type == "function" then
value = value(pos)
elseif (not (value_type == "number")) and
value.render then
value = value:render(pos)
end
if k == 1 then
rslt = rslt + value
else
rslt = rslt / value
end
end
return rslt
end
end
class 'Renderer'
function Renderer:__init(args)
self.args = args
self.type = type(self)
self.base_scaling = args.note and 2^((self.args.note - 69) / 12) or 1
end
-- Precomputed common multipliers.
local sine_factor = TWO_PI * FREQUENCY / SAMPLE_RATE
local phase_factor = FREQUENCY / SAMPLE_RATE
-- "Oscillators"
function sine(pos)
return sin(sine_factor * pos)
end
function triangle(pos)
return 4 * abs((phase_factor * pos) % 1 - 0.5) - 1
end
function square(pos)
return ((phase_factor * pos) % 1) < 0.5 and 1 or -1
end
function sawtooth(pos)
return 2 * ((phase_factor * pos) % 1) - 1
end
function noise(pos)
return random() * 2 - 1
end
-- Factory function returning an adsr
function adsr(attack, decay, sustainLevel, sustainTime, release)
local ad = attack + decay
local ads = ad + sustainTime
local adsr = ads + release
return function(pos)
local t = pos * SAMPLE_RATE_INV -- Convert sample index to seconds.
if t < attack then -- Attack phase: ramp from 0 to 1.
return t / attack
elseif t < ad then -- Decay phase: ramp from 1 to sustainLevel.
return 1 - (t - attack) / decay * (1 - sustainLevel)
elseif t < ads then -- Sustain phase: hold sustainLevel.
return sustainLevel
elseif t < adsr then -- Release phase: ramp down from sustainLevel to 0.
return sustainLevel * (1 - (t - (attack + decay + sustainTime)) / release)
else
return 0
end
end
end
--- Example:
local synth_patch = ADD {
MUL {
ADD {
note = 42,
sawtooth,
},
0.6,
adsr(0.05, 0.2, 0.6, 0.4, 1.0)
},
MUL {
noise,
0.15,
adsr(0.0, 0.05, 0.3, 0.2, 0.8)
}
}
-- render patch to sample
local buffer = renoise.song().selected_sample.sample_buffer
local buffer_set = buffer.set_sample_data
local render_node = synth_patch.render
buffer:prepare_sample_data_changes(false)
for i = 1, buffer.number_of_frames do
buffer_set(buffer, 1, i, render_node(synth_patch, i))
end
buffer:finalize_sample_data_changes()