From 6ee71f4d45a839f17ce6ffa1e44ec5621f1263d9 Mon Sep 17 00:00:00 2001 From: Esa Juhani Ruoho Date: Sat, 21 Sep 2024 23:39:36 +0300 Subject: [PATCH] macro midimappings should no longer error on v3.5.0 --- PakettiExperimental_Verify.lua | 510 +++------------------------------ PakettiLoadPlugins.lua | 12 +- PakettiLoaders.lua | 9 +- PakettiMidi.lua | 497 +++++++++++++++++++++++++++----- PakettiPatternEditor.lua | 121 ++++++++ PakettiRecorder.lua | 5 +- PakettiRequests.lua | 31 ++ preferences.xml | 10 +- 8 files changed, 653 insertions(+), 542 deletions(-) diff --git a/PakettiExperimental_Verify.lua b/PakettiExperimental_Verify.lua index 09c35ab..68c32ae 100644 --- a/PakettiExperimental_Verify.lua +++ b/PakettiExperimental_Verify.lua @@ -1,233 +1,53 @@ +local function flood_fill_column() - - - - - - - - - - - - - - - - - - - - - - - - - - --- Clamp the value between 0.0 and 1.0 -local function clamp_value(value) - return math.max(0.0, math.min(1.0, value)) -end - --- Function to modify the selected device parameter directly or record to automation -function MidiSelectedAutomationParameter(number, message) local song = renoise.song() - local selected_device = song.selected_device - local playback_active = song.transport.playing - local edit_mode = song.transport.edit_mode - local follow_pattern = song.transport.follow_player - - -- Check if a device is selected - if selected_device == nil then - print("No device selected.") - return - end - - -- Validate that the parameter exists at the given index (1-128) - local device_parameter = selected_device.parameters[number] - if device_parameter == nil then - print("No parameter found for index " .. number) - return - end - - -- Clamp the message value - local clamped_message = clamp_value(message) - - -- Always allow editing the device parameter directly with MIDI knobs - device_parameter.value = clamped_message - print("Changed device parameter '" .. device_parameter.name .. - "' directly on device '" .. selected_device.name .. "' to value: " .. tostring(clamped_message)) - - -- Scenario: If Edit Mode = True, write automation - if edit_mode then - -- Get the selected pattern and track - local pattern_index = song.selected_pattern_index - local track_index = song.selected_track_index - - -- Get the track automation for the parameter - local track_automation = song:pattern(pattern_index):track(track_index) - local envelope = track_automation:find_automation(device_parameter) - - -- Create the automation if it doesn't exist - if not envelope then - envelope = track_automation:create_automation(device_parameter) - print("Created new automation for parameter '" .. device_parameter.name .. - "' on device '" .. selected_device.name .. "'") - end - - -- Determine where to write automation: - -- 1. If Playback is OFF, always write to the cursor position (selected line). - -- 2. If Playback is ON: - -- - If Follow Pattern is ON, write to the playhead position. - -- - If Follow Pattern is OFF, write to the cursor position (selected line). - local line_to_write - if not playback_active then - line_to_write = song.selected_line_index -- Write to cursor if Playback is off - elseif follow_pattern then - line_to_write = song.transport.playback_pos.line -- Write to playhead if Follow Pattern is on - else - line_to_write = song.selected_line_index -- Write to cursor if Follow Pattern is off + local track = song.selected_track + local pattern_index = song.selected_pattern_index + local pattern = song.patterns[pattern_index] + local line_index = song.selected_line_index + local lines = pattern.tracks[song.selected_track_index].lines + + local cursor_pos = song.transport.edit_pos + local sel_effect_col = song.selected_effect_column_index + local sel_note_col = song.selected_note_column_index + + -- Check if we are in an effect column + if sel_effect_col ~= 0 then + -- Get the effect value in the current row + local current_effect = lines[line_index].effect_columns[sel_effect_col] + if current_effect.is_empty then + renoise.app():show_status("No effect to flood fill from the current row.") + return end - - -- Record the value to the automation envelope at the determined line - envelope:add_point_at(line_to_write, clamped_message) - - -- Debug output - print("Recorded automation parameter '" .. device_parameter.name .. - "' on device '" .. selected_device.name .. - "' at line " .. line_to_write .. - " to value: " .. tostring(clamped_message)) - end -end - --- Generate MIDI mappings for automation parameters 001-128 -for i = 1, 128 do - renoise.tool():add_midi_mapping{ - name = string.format("Paketti:Selected Device Automation Parameter %03d", i), - invoke = function(message) - -- Normalize the MIDI message (0-127) to a range of 0.0 - 1.0 - local normalized_message = message.int_value / 127 - -- Change device parameter or record to automation based on the logic - MidiSelectedAutomationParameter(i, normalized_message) + -- Loop through rows from current position to the end of the pattern + for i = line_index + 1, #lines do + lines[i].effect_columns[sel_effect_col]:copy_from(current_effect) end - } -end - - - - - - - - - - - - - - - - - -local function clamp_value(value) - -- Ensure the value is clamped between 0.0 and 1.0 - return math.max(0.0, math.min(1.0, value)) -end - -local function record_midi_value(value) - local song = renoise.song() - local automation_parameter = song.selected_automation_parameter - - -- Check if the automation parameter is valid and automatable - if not automation_parameter or not automation_parameter.is_automatable then - renoise.app():show_status("Please select an automatable parameter.") - print("No automatable parameter selected.") - return - end - - -- Find or create the automation envelope for the selected parameter - local track_automation = song:pattern(song.selected_pattern_index):track(song.selected_track_index) - local envelope = track_automation:find_automation(automation_parameter) - - if not envelope then - envelope = track_automation:create_automation(automation_parameter) - print("Created new automation envelope for parameter: " .. automation_parameter.name) - end - - -- Ensure the value is clamped between 0.0 and 1.0 - local clamped_value = clamp_value(value) - - -- Check for a valid selection range - local selection = envelope.selection_range - - -- Case 1: If not playing and a selection exists, clear the selection range and write new points - if not song.transport.playing and selection then - local start_line = selection[1] - local end_line = selection[2] - - -- Clear the range before writing - envelope:clear_range(start_line, end_line) - print("Cleared automation range from line " .. start_line .. " to line " .. end_line) - - -- Write the automation points in the cleared range - for line = start_line, end_line do - local time = line -- Time in pattern lines - -- Create or modify an automation point at the specified time - envelope:add_point_at(time, clamped_value) - print("Added automation point at time: " .. time .. " with value: " .. tostring(clamped_value)) + elseif sel_note_col ~= 0 then + -- Get note column properties (note, instrument, etc.) + local current_note_col = lines[line_index].note_columns[sel_note_col] + if current_note_col.is_empty then + renoise.app():show_status("No note to flood fill from the current row.") + return end - - renoise.app():show_status("Automation points written to cleared selection from line " .. start_line .. " to line " .. end_line) - return -- We return after writing to the selection range - end - - -- Case 2: If playing, Follow Pattern is off, and a selection exists, clear the selection and write - if song.transport.playing and not song.transport.follow_player and selection then - local start_line = selection[1] - local end_line = selection[2] - - -- Clear the range before writing - envelope:clear_range(start_line, end_line) - print("Cleared automation range from line " .. start_line .. " to line " .. end_line) - - -- Write the automation points in the cleared range - for line = start_line, end_line do - local time = line -- Time in pattern lines - -- Create or modify an automation point at the specified time - envelope:add_point_at(time, clamped_value) - print("Added automation point at time: " .. time .. " with value: " .. tostring(clamped_value)) + -- Loop through rows from current position to the end of the pattern + for i = line_index + 1, #lines do + lines[i].note_columns[sel_note_col]:copy_from(current_note_col) end - - renoise.app():show_status("Automation points written to cleared selection from line " .. start_line .. " to line " .. end_line) - return -- We return after writing to the selection range - end - - -- If no selection exists or other conditions aren't met, write to playhead - local playhead_line = song.transport.playback_pos.line - envelope:add_point_at(playhead_line, clamped_value) - renoise.app():show_status("Automation recorded at playhead: " .. playhead_line .. " with value: " .. tostring(clamped_value)) - print("Automation recorded at playhead: " .. playhead_line .. " with value: " .. tostring(clamped_value)) -end --- MIDI mapping function -renoise.tool():add_midi_mapping{ - name = "Paketti:Record Automation to Selected Parameter", - invoke = function(midi_msg) - -- Normalize the MIDI value (0-127) to a range of 0.0 - 1.0 - renoise.song().transport.record_parameter_mode=renoise.Transport.RECORD_PARAMETER_MODE_AUTOMATION - local normalized_value = midi_msg.int_value / 127 - print("Received MIDI value: " .. tostring(midi_msg.int_value) .. " (normalized: " .. tostring(normalized_value) .. ")") - - -- Call the function to record the value - record_midi_value(normalized_value) + else + renoise.app():show_status("Neither an effect nor note column selected.") + return end -} - + -- Return focus to the pattern editor + renoise.app().window.active_middle_frame = renoise.ApplicationWindow.MIDDLE_FRAME_PATTERN_EDITOR + renoise.app():show_status("Flood fill completed.") +end +renoise.tool():add_keybinding{name="Global:Paketti:Flood Fill Column with Row",invoke=function() flood_fill_column() end} @@ -236,6 +56,7 @@ renoise.tool():add_midi_mapping{ +-------- -- Define the path to the mixpaste.xml file within the tool's directory local tool_dir = renoise.tool().bundle_path local xml_file_path = tool_dir .. "mixpaste.xml" @@ -548,259 +369,6 @@ for step = 1, #base_time_divisions do invoke=function() PakettiRepeaterParameters(step, 4) end} -- Mode 4 is Dotted end --------------- --------------------- --------------------------- - - - - - - - - --- Helper function to introduce a short delay (via the idle observer) -local idle_observable = renoise.tool().app_idle_observable -local function short_delay(callback,int_value) - local idle_func -- Declare it beforehand - local idle_count = 0 - idle_func = function() - - if int_value == 0 then - for i, device in ipairs(track.devices) do - if device.display_name == "Repeater" then - device_found = true - device_index = i - device.is_active = false - break - end - end - - - end - - idle_count = idle_count + 1 - if idle_count >= 2 then -- Roughly a millisecond delay - idle_observable:remove_notifier(idle_func) - callback() - end - end - idle_observable:add_notifier(idle_func) -end - --- Define the table for base time divisions, starting with OFF at [1], followed by divisions from 1/1 to 1/128 -local base_time_divisions = { - [1] = "OFF", -- OFF state - [2] = "1 / 1", [3] = "1 / 2", [4] = "1 / 4", [5] = "1 / 8", - [6] = "1 / 16", [7] = "1 / 32", [8] = "1 / 64", [9] = "1 / 128" -} - --- Define the modes as a list for easy cycling (Even, Triplet, Dotted) -local modes = { - [1] = "Even", - [2] = "Triplet", - [3] = "Dotted" -} - --- MIDI mapping logic for the first knob (doesn't change the track name, only shows status) -renoise.tool():add_midi_mapping{ - name = "Paketti:Set Repeater Value x[Knob]", - invoke = function(message) - if message:is_abs_value() then - local int_value = message.int_value - - -- Handle 0-5 as OFF - if int_value <= 5 then - deactivate_repeater(false) -- Pass false to not change the track name - return -- Exit early as it's in the OFF range - end - - -- For 6-127, proceed with normal updates (no track name change) - update_repeater_with_midi_value(int_value, false) - end - end -} - --- MIDI mapping logic for the second knob (changes both the status and track name) -renoise.tool():add_midi_mapping{ - name = "Paketti:Set Repeater Value (Name Tracks) x[Knob]", - invoke = function(message) - if message:is_abs_value() then - local int_value = message.int_value - - -- Handle 0-5 as OFF - if int_value <= 5 then - deactivate_repeater(true) -- Pass true to change the track name - return -- Exit early as it's in the OFF range - end - - -- For 6-127, proceed with normal updates (with track name change) - update_repeater_with_midi_value(int_value, true) - end - end -} - --- Function to deactivate the Repeater without making any other parameter changes --- track_name_change: if true, changes the track name -function deactivate_repeater(track_name_change) - local track = renoise.song().selected_track - local device_found = false - local device_index = nil - - -- Check if the Repeater device already exists on the track - for i, device in ipairs(track.devices) do - if device.display_name == "Repeater" then - device_found = true - device_index = i - break - end - end - - if device_found then - local device = track.devices[device_index] - - -- Deactivate the device without updating parameters - if device.is_active then - device.is_active = false - -- Show status when the repeater is turned off - renoise.app():show_status("Repeater is now Off") - print("Repeater deactivated") - - -- Optionally change track name to "OFF" - if track_name_change then - track.name = "OFF" - end - end - else - print("No Repeater device found on the selected track") - end -end - - --- Function to handle MIDI knob input and update the Repeater accordingly --- track_name_change: if true, changes the track name -function update_repeater_with_midi_value(int_value, track_name_change) - local track = renoise.song().selected_track - local device_found = false - local device_index = nil - - -- Check if the Repeater device already exists on the track - for i, device in ipairs(track.devices) do - if device.display_name == "Repeater" then - device_found = true - device_index = i - break - end - end - - -- If no device is found, insert a new Repeater - if not device_found then - renoise.song().selected_track:insert_device_at("Audio/Effects/Native/Repeater", #track.devices + 1) - device_index = #track.devices -- Index of the newly added device - device_found = true - print("Repeater device added") - end - - if device_found then - -- Get time division and mode based on MIDI value (6 to 127) - local time_division, mode = get_time_division_from_midi(int_value) - - -- Show status of the new division - renoise.app():show_status("Repeater is now " .. time_division .. " " .. mode) - - -- Optionally change the track name to the new division - if track_name_change then - track.name = time_division .. " " .. mode - end - - -- Map mode to the Repeater's mode parameter - local mode_value = 2 -- Default to Even - if mode == "Triplet" then - mode_value = 3 -- Triplet - elseif mode == "Dotted" then - mode_value = 4 -- Dotted - end - - -- Update the Repeater parameters and briefly toggle it off and on - update_and_toggle_repeater(time_division, mode_value, int_value) - end -end - --- Function to update the Repeater with the appropriate parameters, deactivate for a millisecond, and reactivate -function update_and_toggle_repeater(time_division, mode_value, midi_int_value) - local track = renoise.song().selected_track - local device_found = false - local device_index = nil - - -- Check if the Repeater device already exists on the track - for i, device in ipairs(track.devices) do - if device.display_name == "Repeater" then - device_found = true - device_index = i - break - end - end - - if device_found then - local device = track.devices[device_index] - - -- Apply time division and mode - device.parameters[1].value = mode_value -- Set the mode (Even, Triplet, Dotted) - device.parameters[2].value_string = time_division -- Set the time division (1 / 1 to 1 / 128) - - -- Deactivate the device before updating - device.is_active = false - print("Repeater deactivated for parameter update") - - -- Short delay before reactivating the device - short_delay(function() - if midi_int_value ~= 0 then - -- Reactivate the device after a brief delay - device.is_active = true - else - device.is_active = false - print("Repeater remains off") - end - end) - end -end - --- Function to get time division and mode based on MIDI int_value (6 to 127 range) -function get_time_division_from_midi(int_value) - -- Ensure int_value is within 6-127 range - int_value = math.max(6, math.min(int_value, 127)) - - -- Special handling for the last three values: 125, 126, and 127 map to 1/128 Dotted - if int_value >= 125 then - return base_time_divisions[9], modes[3] -- 1/128 Dotted - end - - -- Map the int_value to a spread of 21 steps for divisions up to 1/128 Triplet - local total_steps = 21 -- 7 divisions * 3 modes (Even, Triplet, Dotted), up to 1/128 Triplet - local midi_range = 124 - 6 + 1 -- Total MIDI values to distribute (6 to 124) - local step_size = math.floor(midi_range / total_steps) -- Calculate size per step - - -- Calculate the index in the range (spread evenly) - local index = math.floor((int_value - 6) / step_size) + 1 - - -- Calculate the time division (step) and mode - local step = math.floor((index - 1) / 3) + 2 -- Adjust to map from 1/1 to 1/128 Triplet - local mode = (index - 1) % 3 + 1 -- Mode: Even, Triplet, Dotted - - -- Return the time division and mode - return base_time_divisions[step] or "Unknown Division", modes[mode] or "Unknown Mode" -end - - - - - - - - - - ------------- - diff --git a/PakettiLoadPlugins.lua b/PakettiLoadPlugins.lua index 4d843b5..b180ab7 100644 --- a/PakettiLoadPlugins.lua +++ b/PakettiLoadPlugins.lua @@ -96,14 +96,24 @@ end -- Load Plugin Function function loadPlugin(pluginPath) local selected_index = renoise.song().selected_instrument_index - renoise.song():insert_instrument_at(selected_index + 1) +local currentView = renoise.app().window.active_middle_frame + renoise.song():insert_instrument_at(renoise.song().selected_instrument_index + 1) renoise.song().selected_instrument_index = selected_index + 1 + +if currentView == renoise.ApplicationWindow.MIDDLE_FRAME_INSTRUMENT_PHRASE_EDITOR +then +renoise.app().window.active_middle_frame = renoise.ApplicationWindow.MIDDLE_FRAME_INSTRUMENT_PLUGIN_EDITOR +renoise.app().window.active_middle_frame = renoise.ApplicationWindow.MIDDLE_FRAME_INSTRUMENT_PHRASE_EDITOR +else +renoise.app().window.active_middle_frame = currentView +end local new_instrument = renoise.song().selected_instrument new_instrument.plugin_properties:load_plugin(pluginPath) if new_instrument.plugin_properties.plugin_device and new_instrument.plugin_properties.plugin_device.external_editor_available then new_instrument.plugin_properties.plugin_device.external_editor_visible = true end -- openVisiblePagesToFitParameters() -- Uncomment if you have this function defined elsewhere + end -- Check if any plugins are selected diff --git a/PakettiLoaders.lua b/PakettiLoaders.lua index 58a1c83..2700d31 100644 --- a/PakettiLoaders.lua +++ b/PakettiLoaders.lua @@ -173,6 +173,9 @@ end -- Example usage function LoadPPG() local s = renoise.song() + +local currentView = renoise.app().window.active_middle_frame + -- Ensure an empty instrument slot is selected s.selected_instrument_index = search_empty_instrument() @@ -202,8 +205,10 @@ function LoadPPG() -- Set the active frame and tab for the UI -- renoise.app().window.active_lower_frame = 3 - renoise.app().window.active_middle_frame = 3 - s.selected_instrument.active_tab = 2 +-- renoise.app().window.active_middle_frame = 3 +renoise.app().window.active_middle_frame = currentView + +-- s.selected_instrument.active_tab = storedTab -- Example commented code -- renoise.song().selected_track.devices[checkline].parameters[1].value = 0.474 -- Mix diff --git a/PakettiMidi.lua b/PakettiMidi.lua index bb7aad9..8dc3626 100644 --- a/PakettiMidi.lua +++ b/PakettiMidi.lua @@ -597,71 +597,16 @@ function map_midi_value_to_macro(macro_index, midi_value) renoise.song().selected_instrument.macros[macro_index].value = macro_value end --- Function to add MIDI mappings for each of the 8 macros with custom names -function add_custom_midi_mappings(mapping_names) - -- Ensure renoise.song() is available - if not pcall(renoise.song) then - renoise.app():show_status("No song is currently loaded.") - return - end - - -- Ensure the selected instrument is available - if not renoise.song().selected_instrument then - renoise.app():show_status("No instrument is currently selected.") - return - end - - -- Add MIDI mappings for each of the 8 macros - for macro_index = 1, 8 do - -- Retrieve the custom name for the MIDI mapping - local mapping_name = mapping_names[macro_index] - if mapping_name then - local full_mapping_name = "Paketti:" .. mapping_name - if not added_midi_mappings[full_mapping_name] then - -- Create the MIDI mapping with the custom name - renoise.tool():add_midi_mapping{name=full_mapping_name, invoke=function(midi_message) - -- Extract the MIDI controller value from the MIDI message - local midi_value = midi_message.int_value - -- Map the MIDI value to the macro value - map_midi_value_to_macro(macro_index, midi_value) - end} - -- Track the added MIDI mapping - added_midi_mappings[full_mapping_name] = true - end - else - renoise.app():show_status("Missing name for MIDI mapping " .. macro_index) - end - end -end - --- Custom MIDI mapping names -local midiMacroMappingNames = { - "Midi Selected Instrument Macro 1 (PitchBend)", - "Midi Selected Instrument Macro 2 (Cutoff)", - "Midi Selected Instrument Macro 3 (Resonance)", - "Midi Selected Instrument Macro 4 (Cutoff LfoAmp)", - "Midi Selected Instrument Macro 5 (Cutoff LfoFreq)", - "Midi Selected Instrument Macro 6 (Overdrive)", - "Midi Selected Instrument Macro 7 (ParallelCompression)", - "Midi Selected Instrument Macro 8 (Glide Inertia)" -} - --- Observable to add MIDI mappings when a new song is loaded -renoise.tool().app_new_document_observable:add_notifier(function() - add_custom_midi_mappings(midiMacroMappingNames) -end) - --- Observable to handle document release -renoise.tool().app_release_document_observable:add_notifier(function() - renoise.app():show_status("Song is being released.") -end) +-- Static MIDI mappings for each of the 8 macros +renoise.tool():add_midi_mapping{name="Paketti:Midi Selected Instrument Macro 1 (PitchBend)", invoke=function(midi_message) map_midi_value_to_macro(1, midi_message.int_value) end} +renoise.tool():add_midi_mapping{name="Paketti:Midi Selected Instrument Macro 2 (Cutoff)", invoke=function(midi_message) map_midi_value_to_macro(2, midi_message.int_value) end} +renoise.tool():add_midi_mapping{name="Paketti:Midi Selected Instrument Macro 3 (Resonance)", invoke=function(midi_message) map_midi_value_to_macro(3, midi_message.int_value) end} +renoise.tool():add_midi_mapping{name="Paketti:Midi Selected Instrument Macro 4 (Cutoff LfoAmp)", invoke=function(midi_message) map_midi_value_to_macro(4, midi_message.int_value) end} +renoise.tool():add_midi_mapping{name="Paketti:Midi Selected Instrument Macro 5 (Cutoff LfoFreq)", invoke=function(midi_message) map_midi_value_to_macro(5, midi_message.int_value) end} +renoise.tool():add_midi_mapping{name="Paketti:Midi Selected Instrument Macro 6 (Overdrive)", invoke=function(midi_message) map_midi_value_to_macro(6, midi_message.int_value) end} +renoise.tool():add_midi_mapping{name="Paketti:Midi Selected Instrument Macro 7 (ParallelCompression)", invoke=function(midi_message) map_midi_value_to_macro(7, midi_message.int_value) end} +renoise.tool():add_midi_mapping{name="Paketti:Midi Selected Instrument Macro 8 (Glide Inertia)", invoke=function(midi_message) map_midi_value_to_macro(8, midi_message.int_value) end} --- Initial call to add MIDI mappings if a song is already loaded -if pcall(renoise.song) then - add_custom_midi_mappings(midiMacroMappingNames) -else - renoise.app():show_status("No song is currently loaded at script startup.") -end ---------------- -- Script to map MIDI values to sample modulation set filter types in Renoise -- Ensure this script is named 'Paketti_Midi_Change_Sample_Modulation_Set_Filter.lua' @@ -1505,3 +1450,427 @@ local newInst = currInst - 1 if newInst < 1 then newInst = 1 end renoise.song().selected_instrument_index = newInst end end} + + + +------- +-- Clamp the value between 0.0 and 1.0 +local function clamp_value(value) + return math.max(0.0, math.min(1.0, value)) +end + +-- Function to modify the selected device parameter directly or record to automation +function MidiSelectedAutomationParameter(number, message) + local song = renoise.song() + local selected_device = song.selected_device + local playback_active = song.transport.playing + local edit_mode = song.transport.edit_mode + local follow_pattern = song.transport.follow_player + + -- Check if a device is selected + if selected_device == nil then + print("No device selected.") + return + end + + -- Validate that the parameter exists at the given index (1-128) + local device_parameter = selected_device.parameters[number] + if device_parameter == nil then + print("No parameter found for index " .. number) + return + end + + -- Clamp the message value + local clamped_message = clamp_value(message) + + -- Always allow editing the device parameter directly with MIDI knobs + device_parameter.value = clamped_message + print("Changed device parameter '" .. device_parameter.name .. + "' directly on device '" .. selected_device.name .. "' to value: " .. tostring(clamped_message)) + + -- Scenario: If Edit Mode = True, write automation + if edit_mode then + -- Get the selected pattern and track + local pattern_index = song.selected_pattern_index + local track_index = song.selected_track_index + + -- Get the track automation for the parameter + local track_automation = song:pattern(pattern_index):track(track_index) + local envelope = track_automation:find_automation(device_parameter) + + -- Create the automation if it doesn't exist + if not envelope then + envelope = track_automation:create_automation(device_parameter) + print("Created new automation for parameter '" .. device_parameter.name .. + "' on device '" .. selected_device.name .. "'") + end + + -- Determine where to write automation: + -- 1. If Playback is OFF, always write to the cursor position (selected line). + -- 2. If Playback is ON: + -- - If Follow Pattern is ON, write to the playhead position. + -- - If Follow Pattern is OFF, write to the cursor position (selected line). + local line_to_write + if not playback_active then + line_to_write = song.selected_line_index -- Write to cursor if Playback is off + elseif follow_pattern then + line_to_write = song.transport.playback_pos.line -- Write to playhead if Follow Pattern is on + else + line_to_write = song.selected_line_index -- Write to cursor if Follow Pattern is off + end + + -- Record the value to the automation envelope at the determined line + envelope:add_point_at(line_to_write, clamped_message) + + -- Debug output + print("Recorded automation parameter '" .. device_parameter.name .. + "' on device '" .. selected_device.name .. + "' at line " .. line_to_write .. + " to value: " .. tostring(clamped_message)) + end +end + +-- Generate MIDI mappings for automation parameters 001-128 +for i = 1, 128 do + renoise.tool():add_midi_mapping{ + name = string.format("Paketti:Selected Device Automation Parameter %03d", i), + invoke = function(message) + -- Normalize the MIDI message (0-127) to a range of 0.0 - 1.0 + local normalized_message = message.int_value / 127 + -- Change device parameter or record to automation based on the logic + MidiSelectedAutomationParameter(i, normalized_message) + end + } +end + + + + +------ + + +local function clamp_value(value) + -- Ensure the value is clamped between 0.0 and 1.0 + return math.max(0.0, math.min(1.0, value)) +end + +local function record_midi_value(value) + local song = renoise.song() + local automation_parameter = song.selected_automation_parameter + + -- Check if the automation parameter is valid and automatable + if not automation_parameter or not automation_parameter.is_automatable then + renoise.app():show_status("Please select an automatable parameter.") + print("No automatable parameter selected.") + return + end + + -- Find or create the automation envelope for the selected parameter + local track_automation = song:pattern(song.selected_pattern_index):track(song.selected_track_index) + local envelope = track_automation:find_automation(automation_parameter) + + if not envelope then + envelope = track_automation:create_automation(automation_parameter) + print("Created new automation envelope for parameter: " .. automation_parameter.name) + end + + -- Ensure the value is clamped between 0.0 and 1.0 + local clamped_value = clamp_value(value) + + -- Check for a valid selection range + local selection = envelope.selection_range + + -- Case 1: If not playing and a selection exists, clear the selection range and write new points + if not song.transport.playing and selection then + local start_line = selection[1] + local end_line = selection[2] + + -- Clear the range before writing + envelope:clear_range(start_line, end_line) + print("Cleared automation range from line " .. start_line .. " to line " .. end_line) + + -- Write the automation points in the cleared range + for line = start_line, end_line do + local time = line -- Time in pattern lines + -- Create or modify an automation point at the specified time + envelope:add_point_at(time, clamped_value) + print("Added automation point at time: " .. time .. " with value: " .. tostring(clamped_value)) + end + + renoise.app():show_status("Automation points written to cleared selection from line " .. start_line .. " to line " .. end_line) + return -- We return after writing to the selection range + end + + -- Case 2: If playing, Follow Pattern is off, and a selection exists, clear the selection and write + if song.transport.playing and not song.transport.follow_player and selection then + local start_line = selection[1] + local end_line = selection[2] + + -- Clear the range before writing + envelope:clear_range(start_line, end_line) + print("Cleared automation range from line " .. start_line .. " to line " .. end_line) + + -- Write the automation points in the cleared range + for line = start_line, end_line do + local time = line -- Time in pattern lines + -- Create or modify an automation point at the specified time + envelope:add_point_at(time, clamped_value) + print("Added automation point at time: " .. time .. " with value: " .. tostring(clamped_value)) + end + + renoise.app():show_status("Automation points written to cleared selection from line " .. start_line .. " to line " .. end_line) + return -- We return after writing to the selection range + end + + -- If no selection exists or other conditions aren't met, write to playhead + local playhead_line = song.transport.playback_pos.line + envelope:add_point_at(playhead_line, clamped_value) + renoise.app():show_status("Automation recorded at playhead: " .. playhead_line .. " with value: " .. tostring(clamped_value)) + print("Automation recorded at playhead: " .. playhead_line .. " with value: " .. tostring(clamped_value)) +end + +-- MIDI mapping function +renoise.tool():add_midi_mapping{ + name = "Paketti:Record Automation to Selected Parameter", + invoke = function(midi_msg) + -- Normalize the MIDI value (0-127) to a range of 0.0 - 1.0 + renoise.song().transport.record_parameter_mode=renoise.Transport.RECORD_PARAMETER_MODE_AUTOMATION + local normalized_value = midi_msg.int_value / 127 + print("Received MIDI value: " .. tostring(midi_msg.int_value) .. " (normalized: " .. tostring(normalized_value) .. ")") + + -- Call the function to record the value + record_midi_value(normalized_value) + end +} +------------ +-- Helper function to introduce a short delay (via the idle observer) +local idle_observable = renoise.tool().app_idle_observable +local function short_delay(callback,int_value) + local idle_func -- Declare it beforehand + local idle_count = 0 + idle_func = function() + + if int_value == 0 then + for i, device in ipairs(track.devices) do + if device.display_name == "Repeater" then + device_found = true + device_index = i + device.is_active = false + break + end + end + + + end + + idle_count = idle_count + 1 + if idle_count >= 2 then -- Roughly a millisecond delay + idle_observable:remove_notifier(idle_func) + callback() + end + end + idle_observable:add_notifier(idle_func) +end + +-- Define the table for base time divisions, starting with OFF at [1], followed by divisions from 1/1 to 1/128 +local base_time_divisions = { + [1] = "OFF", -- OFF state + [2] = "1 / 1", [3] = "1 / 2", [4] = "1 / 4", [5] = "1 / 8", + [6] = "1 / 16", [7] = "1 / 32", [8] = "1 / 64", [9] = "1 / 128" +} + +-- Define the modes as a list for easy cycling (Even, Triplet, Dotted) +local modes = { + [1] = "Even", + [2] = "Triplet", + [3] = "Dotted" +} + +-- MIDI mapping logic for the first knob (doesn't change the track name, only shows status) +renoise.tool():add_midi_mapping{ + name = "Paketti:Set Repeater Value x[Knob]", + invoke = function(message) + if message:is_abs_value() then + local int_value = message.int_value + + -- Handle 0-5 as OFF + if int_value <= 5 then + deactivate_repeater(false) -- Pass false to not change the track name + return -- Exit early as it's in the OFF range + end + + -- For 6-127, proceed with normal updates (no track name change) + update_repeater_with_midi_value(int_value, false) + end + end +} + +-- MIDI mapping logic for the second knob (changes both the status and track name) +renoise.tool():add_midi_mapping{ + name = "Paketti:Set Repeater Value (Name Tracks) x[Knob]", + invoke = function(message) + if message:is_abs_value() then + local int_value = message.int_value + + -- Handle 0-5 as OFF + if int_value <= 5 then + deactivate_repeater(true) -- Pass true to change the track name + return -- Exit early as it's in the OFF range + end + + -- For 6-127, proceed with normal updates (with track name change) + update_repeater_with_midi_value(int_value, true) + end + end +} + +-- Function to deactivate the Repeater without making any other parameter changes +-- track_name_change: if true, changes the track name +function deactivate_repeater(track_name_change) + local track = renoise.song().selected_track + local device_found = false + local device_index = nil + + -- Check if the Repeater device already exists on the track + for i, device in ipairs(track.devices) do + if device.display_name == "Repeater" then + device_found = true + device_index = i + break + end + end + + if device_found then + local device = track.devices[device_index] + + -- Deactivate the device without updating parameters + if device.is_active then + device.is_active = false + -- Show status when the repeater is turned off + renoise.app():show_status("Repeater is now Off") + print("Repeater deactivated") + + -- Optionally change track name to "OFF" + if track_name_change then + track.name = "OFF" + end + end + else + print("No Repeater device found on the selected track") + end +end + + +-- Function to handle MIDI knob input and update the Repeater accordingly +-- track_name_change: if true, changes the track name +function update_repeater_with_midi_value(int_value, track_name_change) + local track = renoise.song().selected_track + local device_found = false + local device_index = nil + + -- Check if the Repeater device already exists on the track + for i, device in ipairs(track.devices) do + if device.display_name == "Repeater" then + device_found = true + device_index = i + break + end + end + + -- If no device is found, insert a new Repeater + if not device_found then + renoise.song().selected_track:insert_device_at("Audio/Effects/Native/Repeater", #track.devices + 1) + device_index = #track.devices -- Index of the newly added device + device_found = true + print("Repeater device added") + end + + if device_found then + -- Get time division and mode based on MIDI value (6 to 127) + local time_division, mode = get_time_division_from_midi(int_value) + + -- Show status of the new division + renoise.app():show_status("Repeater is now " .. time_division .. " " .. mode) + + -- Optionally change the track name to the new division + if track_name_change then + track.name = time_division .. " " .. mode + end + + -- Map mode to the Repeater's mode parameter + local mode_value = 2 -- Default to Even + if mode == "Triplet" then + mode_value = 3 -- Triplet + elseif mode == "Dotted" then + mode_value = 4 -- Dotted + end + + -- Update the Repeater parameters and briefly toggle it off and on + update_and_toggle_repeater(time_division, mode_value, int_value) + end +end + +-- Function to update the Repeater with the appropriate parameters, deactivate for a millisecond, and reactivate +function update_and_toggle_repeater(time_division, mode_value, midi_int_value) + local track = renoise.song().selected_track + local device_found = false + local device_index = nil + + -- Check if the Repeater device already exists on the track + for i, device in ipairs(track.devices) do + if device.display_name == "Repeater" then + device_found = true + device_index = i + break + end + end + + if device_found then + local device = track.devices[device_index] + + -- Apply time division and mode + device.parameters[1].value = mode_value -- Set the mode (Even, Triplet, Dotted) + device.parameters[2].value_string = time_division -- Set the time division (1 / 1 to 1 / 128) + + -- Deactivate the device before updating + device.is_active = false + print("Repeater deactivated for parameter update") + + -- Short delay before reactivating the device + short_delay(function() + if midi_int_value ~= 0 then + -- Reactivate the device after a brief delay + device.is_active = true + else + device.is_active = false + print("Repeater remains off") + end + end) + end +end + +-- Function to get time division and mode based on MIDI int_value (6 to 127 range) +function get_time_division_from_midi(int_value) + -- Ensure int_value is within 6-127 range + int_value = math.max(6, math.min(int_value, 127)) + + -- Special handling for the last three values: 125, 126, and 127 map to 1/128 Dotted + if int_value >= 125 then + return base_time_divisions[9], modes[3] -- 1/128 Dotted + end + + -- Map the int_value to a spread of 21 steps for divisions up to 1/128 Triplet + local total_steps = 21 -- 7 divisions * 3 modes (Even, Triplet, Dotted), up to 1/128 Triplet + local midi_range = 124 - 6 + 1 -- Total MIDI values to distribute (6 to 124) + local step_size = math.floor(midi_range / total_steps) -- Calculate size per step + + -- Calculate the index in the range (spread evenly) + local index = math.floor((int_value - 6) / step_size) + 1 + + -- Calculate the time division (step) and mode + local step = math.floor((index - 1) / 3) + 2 -- Adjust to map from 1/1 to 1/128 Triplet + local mode = (index - 1) % 3 + 1 -- Mode: Even, Triplet, Dotted + + -- Return the time division and mode + return base_time_divisions[step] or "Unknown Division", modes[mode] or "Unknown Mode" +end diff --git a/PakettiPatternEditor.lua b/PakettiPatternEditor.lua index 6239c63..a45a9a9 100644 --- a/PakettiPatternEditor.lua +++ b/PakettiPatternEditor.lua @@ -2606,6 +2606,40 @@ renoise.tool():add_keybinding{ end } +renoise.tool():add_keybinding{ + name="Pattern Editor:Paketti:Toggle Note Off on All Tracks on Current Row", + invoke=function() + local song=renoise.song() + local cursor_pos=song.selected_line_index + + -- Iterate over all tracks + for t=1,#song.tracks do + local track=song:track(t) + + -- Skip group, master, and send tracks + if not (track.type==renoise.Track.TRACK_TYPE_GROUP or track.type==renoise.Track.TRACK_TYPE_MASTER or track.type==renoise.Track.TRACK_TYPE_SEND) then + local line=song.patterns[song.selected_pattern_index]:track(t):line(cursor_pos) + local count=track.visible_note_columns + + -- Check if all visible note columns are set to "OFF" + local all_off=true + for i=1,count do + if line.note_columns[i].note_string~="OFF" then + all_off=false + break + end + end + + -- Set or clear "OFF" depending on the result + for i=1,count do + line.note_columns[i].note_string=all_off and "" or "OFF" + end + end + end + end +} + + ------- function PhrasingRandom() @@ -3029,4 +3063,91 @@ renoise.tool():add_keybinding{name="Pattern Editor:Paketti:Delay Column Decrease renoise.tool():add_keybinding{name="Pattern Editor:Paketti:Delay Column Increase Selection/Row (+10)",invoke=function() PakettiDelayColumnModifier(10) end} renoise.tool():add_keybinding{name= "Pattern Editor:Paketti:Delay Column Decrease Selection/Row (-10)", invoke = function() PakettiDelayColumnModifier(-10) end} +------ +function ExposeAndSelectColumn(number) + local song = renoise.song() + local track = song.selected_track + + -- Get the track type + local track_type = track.type + + if track_type == renoise.Track.TRACK_TYPE_SEQUENCER then + -- It's a normal track that can have note and effect columns + + -- Detect if we're on a note column or an effect column + if song.selected_note_column ~= nil then + -- We're on a Note Column + local visNoteCol = track.visible_note_columns + local newVisNoteCol = visNoteCol + number + + if newVisNoteCol > 12 then + renoise.app():show_status("All 12 Note Columns are already visible for the selected track, cannot add more.") + return + elseif newVisNoteCol < 1 then + renoise.app():show_status("Cannot have less than 1 Note Column visible.") + return + end + + -- Update the track's visible note columns + track.visible_note_columns = newVisNoteCol + -- Select the new note column + song.selected_note_column_index = newVisNoteCol + + elseif song.selected_effect_column ~= nil then + -- We're on an Effect Column + local visEffectCol = track.visible_effect_columns + local newVisEffectCol = visEffectCol + number + + if newVisEffectCol > 8 then + renoise.app():show_status("All 8 Effect Columns are already visible for the selected track, cannot add more.") + return + elseif newVisEffectCol < 0 then + renoise.app():show_status("Cannot have less than 0 Effect Columns visible.") + return + end + + -- Update the track's visible effect columns + track.visible_effect_columns = newVisEffectCol + + if newVisEffectCol > 0 then + -- Select the new effect column + song.selected_effect_column_index = newVisEffectCol + else + -- No effect columns visible, deselect any effect column + -- song.selected_effect_column_index = nil + end + + else + renoise.app():show_status("You are not on a Note or Effect Column, doing nothing.") + end + + else + -- The track cannot have note columns, handle effect columns only + local visEffectCol = track.visible_effect_columns + local newVisEffectCol = visEffectCol + number + + if newVisEffectCol > 8 then + renoise.app():show_status("All 8 Effect Columns are already visible for the selected track, cannot add more.") + return + elseif newVisEffectCol < 0 then + renoise.app():show_status("Cannot have less than 0 Effect Columns visible.") + return + end + + -- Update the track's visible effect columns + track.visible_effect_columns = newVisEffectCol + + if newVisEffectCol > 0 then + -- Select the new effect column + song.selected_effect_column_index = newVisEffectCol + else + -- No effect columns visible, deselect any effect column + song.selected_effect_column_index = nil + end + end +end +renoise.tool():add_keybinding{name="Global:Paketti:Expose and Select Next Column",invoke=function() ExposeAndSelectColumn(1) end} +renoise.tool():add_keybinding{name="Global:Paketti:Hide Current and Select Previous Column",invoke=function() ExposeAndSelectColumn(-1) end} +renoise.tool():add_midi_mapping{name="Paketti:Expose and Select Next Column",invoke=function(message) if message:is_trigger() then ExposeAndSelectColumn(1) end end} +renoise.tool():add_midi_mapping{name="Paketti:Hide Current and Select Previous Column",invoke=function(message) if message:is_trigger() then xposeAndSelectColumn(-1) end end} diff --git a/PakettiRecorder.lua b/PakettiRecorder.lua index 25872ce..15da275 100644 --- a/PakettiRecorder.lua +++ b/PakettiRecorder.lua @@ -347,12 +347,15 @@ renoise.tool():add_keybinding{name="Global:Paketti:Simple Play Record Follow (2n -- PD use renoise.tool():add_keybinding{name="Global:Paketti:TouchOSC Sample Recorder and Record", -invoke=function() +invoke=function() +local amf = renoise.app().window.active_middle_frame if renoise.app().window.sample_record_dialog_is_visible then renoise.song().transport:start_stop_sample_recording() else renoise.app().window.sample_record_dialog_is_visible = true renoise.song().transport:start_stop_sample_recording() end +renoise.app().window.active_middle_frame = amf + end} diff --git a/PakettiRequests.lua b/PakettiRequests.lua index 778fe7a..8c4019b 100644 --- a/PakettiRequests.lua +++ b/PakettiRequests.lua @@ -6655,6 +6655,37 @@ renoise.tool():add_keybinding{name="Global:Paketti:Loop Set Percussion",invoke=f renoise.tool():add_keybinding{name="Global:Paketti:Loop Set Texture",invoke=function() PakettiLoopSet("Texture")end} +------- +-- SampleSelector logic +function SampleSelector(step) + local song=renoise.song() + local instrument=song.selected_instrument + local num_samples=#instrument.samples + local current_index=song.selected_sample_index + + if num_samples == 0 then + renoise.app():show_status("There's no sample in this Instrument, doing nothing.") + return + end + + local new_index=current_index+step + + if new_index < 1 then + renoise.app():show_status("You are on the first sample, doing nothing.") + elseif new_index > num_samples then + renoise.app():show_status("You are on the last sample, doing nothing.") + else + song.selected_sample_index=new_index + local sample_name = instrument.samples[new_index].name + local formatted_index = string.format("%03d", new_index) + renoise.app():show_status("Selected Sample " .. formatted_index .. ": " .. sample_name) + end +end + +renoise.tool():add_keybinding{name="Global:Paketti:Select Sample Next",invoke=function()SampleSelector(1)end} +renoise.tool():add_keybinding{name="Global:Paketti:Select Sample Previous",invoke=function()SampleSelector(-1)end} +renoise.tool():add_midi_mapping{name="Paketti:Select Sample Next",invoke=function(message)if message:is_trigger()then SampleSelector(1)end end} +renoise.tool():add_midi_mapping{name="Paketti:Select Sample Previous",invoke=function(message)if message:is_trigger()then SampleSelector(-1)end end} diff --git a/preferences.xml b/preferences.xml index 8910dcf..721b1a3 100644 --- a/preferences.xml +++ b/preferences.xml @@ -62,13 +62,13 @@ 1.0 - 68.65384615384616 + 8.9160839160839238 0.0 false false false true - 63 + 83 88200 LP Moog @@ -149,8 +149,12 @@ Osi-Cyberpunk1 purple poison arrow frog Graphite RC1 + sQeetz_Emerald + Osi-BladeRunner + Osi-TheBlueBrandy + Osi-TrackerHacker - true + false false Darkness A