Live coding sample generation

I made a livecoding environment to manipulate/transform pattern data, and realized what a nice environment this is. (code snippets are cooler than a GUI with modular routing imo)

Can we make a live coding environment to make “synth patches” that generate synth instruments (xrni and only samples)? Preferably in a way that can generate a synth patch in one single statement, and then output+keymap to instruments.

I have a grasp of simple subtractive synthesis (osc+filter) as well as additive synthesis, but have of lately seen more exotic synthesis on this forum.

Maybe it’s time for a general framework that provide basic generators that people can modulate and blend creatively? What do you think?

I can take care of the housekeeping+sandboxing+gui, but the general syntax and statements need to be carefully considered.

(For this to take off, I think it has to be more than a one man show.)

2 Likes

i mean, i don’t think you’re looking for someone to script a sinewave with a loop, are you? you’re looking for far more, right?

Yes. You need to generalize a single simple loop into a class structure that makes it easy to generate various results, and is extendible.

One example is the “composite pattern” (nested calls in a hierarchy) which is a pattern that is seen in renoise.ViewBuilder and is excellent for gui layout (maybe synth too?), for example. Another pattern is using simple classes with methods, which goes a long way too.

I am very inclined towards composite pattern myself. Each nested layer is a “transformation” or a property (like adsr), and every sample can be rendered via the top node with a :render(pos) function.

Using a composite pattern to lay out audio, the two dimensions would manifest as “serial” and “parallel” (parallel being objects that are on the same level, in this case only used for blending or inter-modulation).

PS. One of the reasons I am suggesting this is because 99% of all synths and DAWs still model analog signals. With code you can do much more in terms of synthesis. Negative phase. Modulating/dynamic wavetables. Keytracked downsampling. … And I think this is an area where Renoise could become “interesting”, especially seeing some of the generators presented here over the years and lately.

3 Likes

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? :slight_smile:

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()
1 Like

Hello,
For me, an interesting concept and approach.
It depends on how you want to use it, because even though it is interesting,
I think that users are used to clicking, so the target group will be quite minimal.
But I keep my fingers crossed.

I have also had the idea in my head for a long time that I will write my tools (i.e. their parts such as filters, generators, etc.) modular and in one single tool the user can connect these blocks as they need.
I also have a concept, but it is still early for that.

1 Like