OK so per request I have dug out the step-sequencer script !! It’s really messy, but hopefully someone will have some fun with it. Turning a dial, and watching notes crawl up and down the screen is kind of a ‘wow, is this really still a tracker’ feeling, so I hope you enjoy that
–
Installation
- Take the following code and add it to your GlobalMIDIActions.lua file (remember to take a backup first)
[luabox]------------------------------------------------------------------------------
– Step Sequencer
– length valid range 1-12 (limited by the number of note columns)
– param selected parameter: 1=note, 2=gate, 3=offset, 4=volume
– note valid range 0-121, values 120 & 121 are note-off and empty note
– also, the special value -1 is “strong empty” (skip transposing note)
– gate valid range 0-255 (ticks), 0 is instantly turned off, 255 is always on.
– note that a higher tick value (in lines) than the sequence is ignored
– offset valid range 0-255, 0 is not written to the pattern
– volume valid range 0-127, 127 being full volume and not written to pattern
– adjust affect note/gate/offset/volume
– extend extend output to match track length (boolean)
– global output all voices simultaneously (boolean)
– value# control each step individually
step = {}
step.global = false
step.extend = true
step.modes = {}
step.modes.NOTE = 1
step.modes.GATE = 2
step.modes.OFFSET = 3
step.modes.VOLUME = 4
step.mode = step.modes.NOTE
step.sequence = {}
step.sequence.length = 8
step.sequence.DEFAULT_NOTE_VALUE = -1
step.sequence.DEFAULT_GATE_VALUE = -1
step.sequence.DEFAULT_OFFSET_VALUE = 0
step.sequence.DEFAULT_VOLUME_VALUE = 127
– adjusts note/instrument: transpose an octave down by setting this to -12
step.basenote = 0
step.sequence.note = {}
step.sequence._note = {}
step.sequence.set_note = function(idx,val)
step.sequence.note[idx] = clamp_value(val,-1,121)
step.sequence._note[idx] = step.sequence.note[idx]
end
– adjusts duration: will extend each gate by the selected number of ticks
step.gate = 0
step.sequence.gate = {}
step.sequence._gate = {}
step.sequence.cached_gate = {}
step.sequence.set_gate = function(idx,val)
step.sequence.gate[idx] = clamp_value(val,-1,127)
step.sequence._gate[idx] = step.sequence.gate[idx]
end
– sample offset
step.offset = step.sequence.DEFAULT_OFFSET_VALUE
step.sequence.offset = {}
step.sequence._offset = {}
step.sequence.set_offset = function(idx,val)
step.sequence.offset[idx] = clamp_value(val,0,127)
step.sequence._offset[idx] = step.sequence.offset[idx]
end
– volume: 0 for no sound, 1 for unaltered level
step.volume = 1
step.sequence.volume = {}
step.sequence._volume = {}
step.sequence.set_volume = function(idx,val)
step.sequence.volume[idx] = clamp_value(val,0,127)
step.sequence._volume[idx] = step.sequence.volume[idx]
end
– produce “Fx” hex value
step.ticks_to_notecut = function(val)
return val+240
end
– interpret notecut+line(s)
– @param lines - the number of lines
– @param val - the effect value
– @return number of ticks
step.notecut_to_ticks = function(lines,val)
return (lines*song().transport.tpl)+(val-240)
end
– produce “rhytmic” offset values
step.to_discrete_steps = function(val)
return math.floor(((val+1)*2)/16)*16
end
– get the currently focused column
step.get_selected_column = function()
return clamp_value(song().selected_note_column_index,1,step.sequence.length)
end
– display useful information
step.show_status = function(str)
app():show_status(str)
end
– sequence.clear()
step.sequence.clear = function()
for idx = 1,step.sequence.length do
step.sequence.set_note(idx,step.sequence.DEFAULT_NOTE_VALUE)
step.sequence.set_gate(idx,step.sequence.DEFAULT_GATE_VALUE)
step.sequence.set_offset(idx,step.sequence.DEFAULT_OFFSET_VALUE)
step.sequence.set_volume(idx,step.sequence.DEFAULT_VOLUME_VALUE)
end
end
– sequence.set_length()
– supply default values for undefined indices, skip existing values
step.sequence.set_length = function(new_length)
for idx = 1,new_length do
if(step.sequence.note[idx]==nil)then
step.sequence.set_note(idx,step.sequence.DEFAULT_NOTE_VALUE)
end
if(step.sequence.gate[idx]==nil)then
step.sequence.set_gate(idx,step.sequence.DEFAULT_GATE_VALUE)
end
if(step.sequence.offset[idx]==nil)then
step.sequence.set_offset(idx,step.sequence.DEFAULT_OFFSET_VALUE)
end
if(step.sequence.volume[idx]==nil)then
step.sequence.set_volume(idx,step.sequence.DEFAULT_VOLUME_VALUE)
end
end
step.sequence.length = new_length
end
– show_note_column
– called when data is written to the pattern to ensure that columns are visible
– todo: make optional (advanced settings)
step.show_note_column = function(idx)
if idx > renoise.song().selected_track.visible_note_columns then
renoise.song().selected_track.visible_note_columns = idx
end
end
– output_sequence
– @mask_col - optional, column == sequencer index
– @mask_type - optional, int or nil
step.output_sequence = function(mask_col,mask_type)
if not song().transport.edit_mode then
return
end
if step.global == true then
mask_col = nil
end
local tpl = song().transport.tpl
local instr_index = song().selected_instrument_index-1
local playback_line = song().transport.edit_pos.line
local curr_line = 1
local offset = nil
local show_column = nil
local col_index = nil
local note_set,gate_set,offset_set,volume_set
local set_note,set_gate,set_offset,set_volume
local writeahead_length = step.sequence.length-1
if step.extend then
writeahead_length = 512
end
local iter = renoise.song().pattern_iterator:lines_in_pattern_track(
song().selected_pattern_index,
song().selected_track_index);
if mask_type then
set_note = (mask_type == step.modes.NOTE)
set_gate = (mask_type == step.modes.GATE)
set_offset = (mask_type == step.modes.OFFSET)
set_volume = (mask_type == step.modes.VOLUME)
else
set_note = true
set_gate = true
set_offset = true
set_volume = true
end
for pos,line in iter do
if(curr_line>=playback_line) then
if(writeahead_length<0) then
break
else
offset = curr_line%step.sequence.length
if(offset==0) then – fix
offset = step.sequence.length
end
– process note columns
col_index = 1
for i,note_column in ipairs(line.note_columns) do
– using masked mode? (global will always continue)
local continue = true
if not step.global then
if mask_col and mask_col ~= col_index then
continue = false
end
end
if continue then
show_column = false
note_set = false
gate_set = false
volume_set = false
– do stuff on the trigger point
if(offset==col_index ) then
if set_note then
local val = step.sequence.note[offset]
if(val==-1) then
– skip undefined notes
elseif(val>120) then
– don’t adjust when set to note-off or empty
note_column.note_value = val
note_column.instrument_value = 255
else
note_column.note_value = clamp_value(val+step.basenote,0,121)
note_column.instrument_value = instr_index
show_column = true
end
note_set = true
end
if set_volume then
local val = math.floor(step.sequence.volume[offset]*step.volume)
if val==127 or val==255 then
note_column.volume_value = 255
else
note_column.volume_value = clamp_value(val,0,127)
show_column = true
end
volume_set = true
end
if set_gate then
step.sequence.cached_gate[col_index] = {}
local val = step.sequence.gate[offset]+step.gate
if val==255 or val==-1 then
– don’t bother
elseif(val<tpl) then
– insert the notecut right away
note_column.panning_value = step.ticks_to_notecut(val)
step.sequence.cached_gate[col_index][col_index] = val
gate_set = true
show_column = true
else
– insert the notecut later
local tick_value = val%tpl
local row_value = math.floor(val/tpl)+1%step.sequence.length
if(row_value<=step.sequence.length) then
local target_row = ((row_value+(col_index-1))%step.sequence.length)
if(target_row==0) then --fix
target_row = step.sequence.length
end
step.sequence.cached_gate[col_index][target_row] = tick_value
end
end
end
else
if set_gate then
– look for scheduled notecut
if(step.sequence.cached_gate[col_index]) then
local tmp_value = step.sequence.cached_gate[col_index][offset]
if(tmp_value) then
note_column.panning_value = step.ticks_to_notecut(tmp_value)
gate_set = true
show_column = true
end
end
end
end
– clear existing?
if set_gate and not gate_set then
note_column.panning_value = 255
end
if set_note and not note_set then
note_column.note_value = 121
note_column.instrument_value = 255
end
if set_volume and not volume_set then
note_column.volume_value = 255
end
if show_column then
step.show_note_column(col_index)
end
if gate_set then
renoise.song().selected_track.panning_column_visible = true
end
end
col_index = col_index+1
end
– process effect columns
col_index = 1
for i,fx_column in ipairs(line.effect_columns) do
if set_offset and col_index == 1 then
local continue = true
if not step.global then
local val = curr_line%step.sequence.length
if(val==0) then – fix
val = step.sequence.length
end
if(mask_col and mask_col~=val) then
continue = false
end
end
if(continue) then
local val = step.to_discrete_steps(step.sequence.offset[offset])
– fix : 256 becoming 0 when wrapped
if val==256 then
val=255
end
val = wrap_value(val+step.offset,0,255)
if val ~= 0 then
fx_column.number_value = 9
fx_column.amount_value = val
else
fx_column.number_value = 0
fx_column.amount_value = 0
end
end
end
col_index = col_index+1
end
end
writeahead_length = writeahead_length-1
end
curr_line = curr_line+1
end
end
– learn_sequence() : import a sequence from actual pattern data,
– tries to establish the actual values (including adjustments)
– but some loss of precision might occur (for example, a low master volume
– might reduce the precision of the resulting volume levels)
– Note also that global mode will make this method “greedy” (analyze all),
– while the non-global mode will only attempt to the current parameter
step.learn_sequence = function()
local playback_line = song().transport.edit_pos.line
local curr_line = 1
local val = nil
local offset = nil
local col_index = nil
local get_note,get_gate,get_offset,get_volume
local readahead_length = step.sequence.length-1
local iter = renoise.song().pattern_iterator:lines_in_pattern_track(
song().selected_pattern_index,
song().selected_track_index);
if step.global then
get_note = true
get_gate = true
get_offset = true
get_volume = true
step.sequence.clear()
else
get_note = (step.mode == step.modes.NOTE)
get_gate = (step.mode == step.modes.GATE)
get_offset = (step.mode == step.modes.OFFSET)
get_volume = (step.mode == step.modes.VOLUME)
end
for pos,line in iter do
if(curr_line>=playback_line) then
if(readahead_length<0) then
break
else
offset = curr_line%step.sequence.length
if(offset==0) then – fix
offset = step.sequence.length
end
– process note columns
col_index = 1
for i,note_column in ipairs(line.note_columns) do
if(offset==col_index ) then
– do stuff on the trigger point
if get_note then
if step.basenote > 0 then
val = note_column.note_value-step.basenote
else
val = note_column.note_value+math.abs(step.basenote)
end
step.sequence.set_note(offset,val)
end
if get_volume then
val = note_column.volume_value
– empty (255) is the same as full volume (127&128)
if val==255 or val==128 then
val = 127
else
val = val*step.volume
end
step.sequence.volume[offset] = note_column.volume_value
end
if get_gate then
val = note_column.panning_value
if val ~= 255 and val > 239 then
step.sequence.gate[offset] = val-240
end
end
else
– non-trigger point position
if get_gate then
val = note_column.panning_value
– proceed only if not special ‘blank’ value
if val ~= 255 and val > 239 then
– consider the master-duration
val=val-step.gate
if curr_line>=col_index then
– after note
step.sequence.set_gate(col_index,step.notecut_to_ticks(curr_line-col_index,val))
else
– before note
local lines = step.sequence.length-(col_index-curr_line)
step.sequence.set_gate(col_index,step.notecut_to_ticks(lines,val))
end
end
end
end
col_index = col_index+1
end
– process effect columns
col_index = 1
for i,fx_column in ipairs(line.effect_columns) do
if get_offset and col_index == 1 then
– note that loss of precision might occur if the offset is stepped
local val = math.floor((fx_column.amount_value/2)-(step.offset/2))
step.sequence.offset[offset] = wrap_value(val,0,255)
end
col_index = col_index+1
end
end
readahead_length = readahead_length-1
end
curr_line = curr_line+1
end
end
add_action(“Step sequencer:Length [Set]”,
function(message)
if message:is_abs_value() then
if(message.int_value == clamp_value(message.int_value,1,12))then
step.sequence.set_length(message.int_value)
step.show_status("Step sequencer: sequence length set to "…val)
end
end
end)
add_action(“Step sequencer:Param (1=Note, 2=Gate, 3=Offset, 4=Vol)”,
function(message)
if message:is_abs_value() then
if(message.int_value == clamp_value(message.int_value,1,4))then
step.mode = message.int_value
if step.mode == 1 then
step.show_status(“Step sequencer: active parameter = pitch”)
elseif step.mode == 2 then
step.show_status(“Step sequencer: active parameter = duration”)
elseif step.mode == 3 then
step.show_status(“Step sequencer: active parameter = sample-offset”)
elseif step.mode == 4 then
step.show_status(“Step sequencer: active parameter = volume”)
end
end
end
end)
add_action(“Step sequencer:Adjust value [Set]”,
function(message)
if message:is_abs_value() then
local idx = step.get_selected_column()
if(step.mode==step.modes.NOTE)then
– adjust +/- 12 semitones
local val = math.floor((message.int_value-64)/5.333)+1
if(step.global)then
step.basenote = val
step.show_status("Step sequencer: master-pitch changed to “…val)
else
if step.sequence.note[idx] == -1 then
return
end
step.sequence.note[idx] = clamp_value(step.sequence._note[idx]+val,0,121)
step.show_status(“Step sequencer: pitch #”…idx…” was set to "…step.sequence.note[idx])
end
end
if step.mode == step.modes.GATE then
local val = message.int_value
if step.global then
step.gate = val
step.show_status(“Step sequencer: master-duration changed to “…val)
else
step.sequence.gate[idx] = clamp_value(step.sequence._gate[idx]+val,0,255)
step.show_status(“Step sequencer: duration #”…idx…” was set to “…step.sequence.gate[idx]…” ticks”)
end
end
if step.mode == step.modes.OFFSET then
local val = message.int_value
if step.global then
step.offset = step.to_discrete_steps(val)
step.show_status("Step sequencer: master sample-offset changed to “…val)
else
step.sequence.offset[idx] = wrap_value(step.sequence._offset[idx]+val,0,255)
step.show_status(“Step sequencer: sample-offset #”…idx…” was set to "…step.sequence.offset[idx])
end
end
if step.mode == step.modes.VOLUME then
– adjust : assume absolute 7 bit value
local val = message.int_value/127
if step.global then
step.volume = val
step.show_status("Step sequencer: master-volume changed to “…val)
else
step.sequence.volume[idx] = clamp_value(math.floor(step.sequence._volume[idx]*val),0,127)
step.show_status(“Step sequencer: volume #”…idx…” was set to "…step.sequence.volume[idx])
end
end
step.output_sequence(idx,step.mode)
end
end)
add_action(“Step sequencer:Output [Trigger]”,
function(message)
if message:is_trigger() then
step.output_sequence(step.get_selected_column(song().selected_note_column_index))
step.show_status(“Step sequencer: Output()”)
end
end)
add_action(“Step sequencer:Learn [Trigger]”,
function(message)
if message:is_trigger() then
step.learn_sequence()
step.show_status(“Step sequencer: Learn()”)
end
end)
add_action(“Step sequencer:Global mode [Set]”,
function(message)
step.global = boolean_message_value(message)
if(step.global) then
step.show_status(“Step sequencer: Global mode enabled”)
else
step.show_status(“Step sequencer: Global mode disabled”)
end
if step.global then
– apply the manually adjusted values
if(step.mode==step.modes.NOTE)then
for idx = 1,step.sequence.length do
step.sequence._note[idx] = step.sequence.note[idx]
end
end
if(step.mode==step.modes.GATE)then
for idx = 1,step.sequence.length do
step.sequence._gate[idx] = step.sequence.gate[idx]
end
end
if(step.mode==step.modes.OFFSET)then
for idx = 1,step.sequence.length do
step.sequence._offset[idx] = step.sequence.offset[idx]
end
end
if(step.mode==step.modes.VOLUME)then
for idx = 1,step.sequence.length do
step.sequence._volume[idx] = step.sequence.volume[idx]
end
end
end
end)
add_action(“Step sequencer:Extend mode [Set]”,
function(message)
step.extend = boolean_message_value(message)
if(step.extend) then
step.show_status(“Step sequencer: Extended mode enabled”)
else
step.show_status(“Step sequencer: Extended mode disabled”)
end
end)
for seq_index = 1,12 do
add_action(string.format(
“Step sequencer:Step #%02d [Set]”, seq_index),
function(message)
if message:is_abs_value() then
if(step.mode==step.modes.NOTE)then
step.sequence.set_note(seq_index,message.int_value)
step.show_status(“Step sequencer: pitch #”…seq_index…" was set to “…message.int_value)
elseif(step.mode==step.modes.GATE)then
step.sequence.set_gate(seq_index,message.int_value)
step.show_status(“Step sequencer: duration #”…seq_index…” was set to “…message.int_value…” ticks")
elseif(step.mode==step.modes.OFFSET)then
step.sequence.set_offset(seq_index,message.int_value)
step.show_status(“Step sequencer: sample-offset #”…seq_index…" was set to “…message.int_value)
elseif(step.mode==step.modes.VOLUME)then
step.sequence.set_volume(seq_index,message.int_value)
step.show_status(“Step sequencer: volume #”…seq_index…” was set to "…message.int_value)
end
step.output_sequence(seq_index,step.mode)
end
end)
end
– now create the default sequence
step.sequence.set_length(step.sequence.length)
[/luabox]
- Restart Renoise, or reload mappings from the MIDI mappings dialog.
- Assign the mappings to a MIDI controller using the MIDI mappings dialog
(As an example, this my Nocturn mapped with the sequencer)
–
Quick tutorial:
Basic requirements for the step sequencer to work, is to have a track containing several note columns (12),
a visible panning column (if you want to be able to see the note duration), and a single effect column for
holding the offset commands.
Whenever a parameter is changed, the script will output notes to the pattern. The length of the output is
determined by two things: the length of the sequence itself, and the “extend mode”, which will cause the
output to occupy the entire track from the editing position.
Oh, and you need to turn on edit-mode before anything is written to the pattern
This is the basic operation of the step sequencer - you can turn some dials to get sound, and it’s recorded
into the pattern exactly like it’s playing (well, almost). The remaining features like global mode and adjust
are much more ninja in nature, but they are really useful once you’ve gotten used to the basic concept.
Global mode : each time a parameter is changed, the sequence is output. This is useful, if you are recording
into several empty patterns and want to capture the entire output. It also affects how “adjust” works
Adjust : change the current value in a number of ways, depending on the type (note/volume/etc). The
active note-column in Renoise determine which value is targeted (since each column corresponds to a step
in the sequence, this is quite easy). In global mode, the current note-column isn’t important, as all columns
are affected.
Adjustments vary slightly for the different parameter types :
- Note: transposed +/- 12 semitones - Gate: note duration extended with #ticks - Offset: the value is rotated (for rhytmic purposes) - Volume: each step is multiplied with this value
Learn : attempt to import a sequence, so whatever data is present at the cursor position in the current pattern/track/line. Therefore, “learning” a blank pattern will clear the script memory
Output: will output the sequence (this also happens when changing each step)
Distance: the interval between notes (also when importing)
Extend: extend the output, to fill the entire pattern
Adjust value: a global adjust, like offset on all Steps