-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathPakettiHyperEdit.lua
More file actions
3990 lines (3394 loc) · 154 KB
/
PakettiHyperEdit.lua
File metadata and controls
3990 lines (3394 loc) · 154 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- PakettiHyperEdit.lua
-- 8-Row Interchangeable Stepsequencer with individual device/parameter selection
-- Each row has its own canvas with device and parameter dropdowns
-- Debug control flag - set to true to enable verbose logging
local DEBUG_HYPEREDIT = false
-- Helper function to clean parameter names by removing "CC XX " prefix
-- e.g., "CC 1 (Mod Wheel)" becomes "Mod Wheel"
function PakettiHyperEditCleanParameterName(param_name)
if not param_name then
return param_name
end
-- Remove "CC XX " pattern (e.g., "CC 54 (Cutoff)" becomes "(Cutoff)")
local cleaned = param_name:gsub("^CC %d+ ", "")
-- Remove parentheses if the entire remaining string is wrapped in them
-- e.g., "(Cutoff)" becomes "Cutoff"
if cleaned:match("^%((.+)%)$") then
cleaned = cleaned:match("^%((.+)%)$")
end
return cleaned
end
-- Function to find best available instrument control device with priority:
-- 1. *Instr. MIDI Control (highest priority)
-- 2. *Instr. Automation (medium priority)
-- 3. *Instr. Macros (lowest priority - fallback)
function PakettiHyperEditFindBestInstrControlDevice(track, expected_display_name)
local device_priorities = {
{name = "*Instr. MIDI Control", path = "Audio/Effects/Native/*Instr. MIDI Control"},
{name = "*Instr. Automation", path = "Audio/Effects/Native/*Instr. Automation"},
{name = "*Instr. Macros", path = "Audio/Effects/Native/*Instr. Macros"}
}
for priority, device_info in ipairs(device_priorities) do
for i, device in ipairs(track.devices) do
-- Check for device name OR display name match (for renamed devices)
if device.name == device_info.name or
(expected_display_name and device.display_name == expected_display_name) then
print("HYPEREDIT DEVICE PRIORITY: Found " .. device_info.name .. " device at index " .. i .. " (priority " .. priority .. ")")
return {
device = device,
index = i,
priority = priority,
device_type = device_info.name
}
end
end
end
print("HYPEREDIT DEVICE PRIORITY: No instrument control device found")
return nil
end
-- Function to create best available instrument control device with priority
function PakettiHyperEditCreateBestInstrControlDevice(track, expected_display_name)
local device_priorities = {
{name = "*Instr. MIDI Control", path = "Audio/Effects/Native/*Instr. MIDI Control"},
{name = "*Instr. Automation", path = "Audio/Effects/Native/*Instr. Automation"},
{name = "*Instr. Macros", path = "Audio/Effects/Native/*Instr. Macros"}
}
-- Check if any priority device already exists - if so, don't create another
local existing = PakettiHyperEditFindBestInstrControlDevice(track, expected_display_name)
if existing then
return existing
end
-- Create the highest priority device (MIDI Control) if none exist
local device_info = device_priorities[1] -- *Instr. MIDI Control
print("HYPEREDIT DEVICE PRIORITY: Creating " .. device_info.name .. " device")
local ok, err = pcall(function()
track:insert_device_at(device_info.path, #track.devices + 1)
end)
if not ok then
print("HYPEREDIT ERROR: Failed to create " .. device_info.name .. " device: " .. tostring(err))
return nil
end
local new_device = track:device(#track.devices)
if expected_display_name then
new_device.display_name = expected_display_name
end
new_device.is_maximized = false
return {
device = new_device,
index = #track.devices,
priority = 1,
device_type = device_info.name
}
end
-- Device parameter whitelists for cleaner parameter selection
local DEVICE_PARAMETER_WHITELISTS = {
["AU: Valhalla DSP, LLC: ValhallaDelay"] = {
"Mix",
"Feedback",
"DelayL_Ms",
"DelayR_Ms",
"DelayStyle",
"Width",
"Age",
"DriveIn",
"ModRate",
"ModDepth",
"LowCut",
"HighCut",
"Diffusion",
"Mode",
"Era"
},
["Wavetable Mod *LFO"] = {
"Amplitude",
"Frequency",
"Offset"
},
["*Instr. MIDI Control"] = {
-- Dynamic parameters - will be queried from device
},
["*Instr. Automation"] = {
-- Dynamic parameters - will be queried from device
},
["*Instr. Macros"] = {
"Cutoff",
"Resonance",
"Pitchbend",
"Drive",
"ParallelComp",
"PB Inertia",
"CutLfoAmp",
"CutLfoFreq",
},
["Gainer"] = {
"Gain"
}
}
local vb = renoise.ViewBuilder()
-- Global flag to prevent auto-read during device list updates
local is_updating_device_lists = false
local row_devices = {} -- [row] = selected device
local row_parameters = {} -- [row] = selected parameter
local device_lists = {} -- [row] = available devices for that row
local parameter_lists = {} -- [row] = available parameters for selected device
-- Observers
local track_change_notifier = nil
local device_change_notifier = nil
-- Constants
MAX_STEPS = 256 -- Global, support up to 256 steps
-- Dynamic row count - will be set from preferences
local NUM_ROWS = 8
-- Update NUM_ROWS from preferences
function PakettiHyperEditUpdateRowCount()
-- Initialize preferences if they don't exist
if preferences then
if not preferences.PakettiHyperEditRowCount then
preferences:add_property("PakettiHyperEditRowCount", renoise.Document.ObservableNumber(8))
preferences:save_as("preferences.xml")
end
if not preferences.PakettiHyperEditAutoFit then
preferences:add_property("PakettiHyperEditAutoFit", renoise.Document.ObservableBoolean(true))
preferences:save_as("preferences.xml")
end
if not preferences.PakettiHyperEditManualRows then
preferences:add_property("PakettiHyperEditManualRows", renoise.Document.ObservableNumber(8))
preferences:save_as("preferences.xml")
end
end
-- Use manual row count if auto-fit is disabled, otherwise use saved row count
if preferences and preferences.PakettiHyperEditAutoFit and not preferences.PakettiHyperEditAutoFit.value then
-- Auto-fit disabled: use manual row count (capped at 16 to prevent dialog being too large)
NUM_ROWS = math.min(16, preferences.PakettiHyperEditManualRows.value)
elseif preferences and preferences.PakettiHyperEditRowCount then
-- Auto-fit enabled: use saved row count (capped at 16 to prevent dialog being too large)
NUM_ROWS = math.min(16, preferences.PakettiHyperEditRowCount.value)
else
NUM_ROWS = 8 -- Default fallback
end
end
-- Dialog state
local hyperedit_dialog = nil
local dialog_vb = nil -- Store ViewBuilder instance
local row_canvases = {} -- [row] = canvas
local pre_configuration_applied = false
-- Playhead variables (like PakettiGater)
local playhead_timer_fn = nil
local playing_observer_fn = nil
local playhead_step_indices = {} -- [row] = current_step
local playhead_color = nil
-- Row state variables (must be declared early for playhead functions)
local row_steps = {} -- [row] = step count for this row (individual per row)
-- Track color capture state is now stored in preferences (PakettiHyperEditCaptureTrackColor)
-- Get current track color with blending
function PakettiHyperEditGetTrackColor()
local song = renoise.song()
if not song then return {120, 40, 160} end -- Default purple
local track = song.selected_track
if not track then return {120, 40, 160} end
-- Get track color (RGB 0-255) and blend amount (0-100)
local track_color = track.color -- RGB array
local color_blend = track.color_blend or 50 -- Default 50% blend
-- When blend is 0%, it means "no blending" in Renoise - use raw color with boost
if color_blend == 0 then
local boosted_color = {
math.max(track_color[1], 100), -- Ensure minimum 100 for visibility
math.max(track_color[2], 100),
math.max(track_color[3], 100)
}
return boosted_color
end
-- For non-zero blend, use normal blending
local min_brightness = 80 -- Minimum component value for visibility
local boosted_color = {
math.max(track_color[1], min_brightness),
math.max(track_color[2], min_brightness),
math.max(track_color[3], min_brightness)
}
-- Use actual blend percentage (don't force minimum)
local effective_blend = color_blend / 100.0
-- Apply blending with darker background for contrast
local background = {30, 30, 30} -- Slightly lighter background for better contrast
local blended_color = {
math.floor(boosted_color[1] * effective_blend + background[1] * (1 - effective_blend)),
math.floor(boosted_color[2] * effective_blend + background[2] * (1 - effective_blend)),
math.floor(boosted_color[3] * effective_blend + background[3] * (1 - effective_blend))
}
return blended_color
end
-- Update colors based on capture track color setting
function PakettiHyperEditUpdateColors()
if preferences.PakettiHyperEditCaptureTrackColor.value then
local track_color = PakettiHyperEditGetTrackColor()
COLOR_ACTIVE_STEP = {track_color[1], track_color[2], track_color[3], 255}
else
COLOR_ACTIVE_STEP = {120, 40, 160, 255} -- Default purple
end
-- Update playhead color too
playhead_color = PakettiHyperEditResolvePlayheadColor()
-- Update all canvases
-- Update all step count button colors
for row = 1, NUM_ROWS do
PakettiHyperEditUpdateStepButtonColors(row)
end
local updated_count = 0
for row = 1, NUM_ROWS do
if row_canvases[row] then
row_canvases[row]:update()
updated_count = updated_count + 1
end
end
end
-- Playhead color resolution function
function PakettiHyperEditResolvePlayheadColor()
-- If track color capture is enabled, use track color for playhead too
if preferences.PakettiHyperEditCaptureTrackColor.value then
local track_color = PakettiHyperEditGetTrackColor()
-- Make playhead brighter than active steps for visibility
local playhead_result = {
math.min(255, track_color[1] + 60),
math.min(255, track_color[2] + 60),
math.min(255, track_color[3] + 60)
}
return playhead_result
end
-- Otherwise use preferences
local choice = (preferences and preferences.PakettiGrooveboxPlayheadColor and preferences.PakettiGrooveboxPlayheadColor.value) or 2
if choice == 1 then return nil end -- None
if choice == 2 then return {255,128,0} end -- Bright Orange
if choice == 3 then return {64,0,96} end -- Deeper Purple
if choice == 4 then return {0,0,0} end -- Black
if choice == 5 then return {255,255,255} end -- White
if choice == 6 then return {64,64,64} end -- Dark Grey
return {255,128,0}
end
-- Playhead update functions (like PakettiGater)
function PakettiHyperEditUpdatePlayheadHighlights()
if not hyperedit_dialog or not hyperedit_dialog.visible then return end
local song = renoise.song()
if not song then return end
local current_line = song.selected_line_index
if song.transport.playing then
local pos = song.transport.playback_pos
if pos and pos.line then current_line = pos.line end
end
if not current_line then return end
-- Update playhead for each row based on its individual step count
local needs_update = false
for row = 1, NUM_ROWS do
local row_step_count = row_steps[row] or 16
local step_index = ((current_line - 1) % row_step_count) + 1
if playhead_step_indices[row] ~= step_index then
playhead_step_indices[row] = step_index
needs_update = true
-- Update the canvas for this row
if row_canvases[row] then
row_canvases[row]:update()
end
end
end
end
function PakettiHyperEditSetupPlayhead()
local song = renoise.song()
if not song then return end
playhead_color = PakettiHyperEditResolvePlayheadColor()
if not playhead_timer_fn then
playhead_timer_fn = function()
PakettiHyperEditUpdatePlayheadHighlights()
end
renoise.tool():add_timer(playhead_timer_fn, 40) -- 25 FPS
end
if not playing_observer_fn then
playing_observer_fn = function()
playhead_color = PakettiHyperEditResolvePlayheadColor()
PakettiHyperEditUpdatePlayheadHighlights()
end
if song.transport.playing_observable and not song.transport.playing_observable:has_notifier(playing_observer_fn) then
song.transport.playing_observable:add_notifier(playing_observer_fn)
end
end
end
function PakettiHyperEditCleanupPlayhead()
if playhead_timer_fn then
if renoise.tool():has_timer(playhead_timer_fn) then
renoise.tool():remove_timer(playhead_timer_fn)
end
playhead_timer_fn = nil
end
local song = renoise.song()
if song and playing_observer_fn then
pcall(function()
if song.transport.playing_observable and song.transport.playing_observable:has_notifier(playing_observer_fn) then
song.transport.playing_observable:remove_notifier(playing_observer_fn)
end
end)
playing_observer_fn = nil
end
playhead_step_indices = {}
end
-- Detect shortest repeating pattern in automation points
function PakettiHyperEditDetectPatternLength(automation_points)
if not automation_points or #automation_points == 0 then
return 16 -- Default
end
-- Convert points to a step/value map for easier analysis
local step_values = {}
local max_step = 0
for _, point in ipairs(automation_points) do
local step = point.time
local value = point.value
if step >= 1 and step <= 256 then -- Only consider first 256 steps
step_values[step] = value
if step > max_step then
max_step = step
end
end
end
if max_step <= 1 then
return 16 -- Not enough data, default to 16
end
-- Test different pattern lengths starting from shortest
for pattern_length = 1, 256 do
local is_repeating = true
-- Check if this pattern length creates a repeating cycle
for test_step = 1, max_step do
local base_step = ((test_step - 1) % pattern_length) + 1
local base_value = step_values[base_step]
local current_value = step_values[test_step]
-- If both steps have values, they must match for pattern to be valid
if base_value and current_value then
-- Allow small tolerance for floating point comparison
if math.abs(base_value - current_value) > 0.001 then
is_repeating = false
break
end
elseif base_value or current_value then
-- One has a value, the other doesn't - not a match
is_repeating = false
break
end
end
if is_repeating then
print("DEBUG: Detected repeating pattern of " .. pattern_length .. " steps (max step: " .. max_step .. ")")
return pattern_length
end
end
-- No repeating pattern found, use max step count or default
if max_step <= 16 then
return 16
elseif max_step <= 32 then
return 32
elseif max_step <= 48 then
return 48
elseif max_step <= 64 then
return 64
elseif max_step <= 96 then
return 96
elseif max_step <= 112 then
return 112
elseif max_step <= 128 then
return 128
elseif max_step <= 192 then
return 192
else
return 256
end
end
-- Set all steps in row to a specific value
function PakettiHyperEditSetAllStepsToValue(row, value)
print("DEBUG: PakettiHyperEditSetAllStepsToValue called - row: " .. row .. ", value: " .. value)
if not step_data[row] then
print("DEBUG: step_data[" .. row .. "] does not exist, initializing...")
step_data[row] = {}
step_active[row] = {}
end
if not row_parameters[row] then
renoise.app():show_status("HyperEdit Row " .. row .. ": Select parameter first")
print("DEBUG: No parameter selected for row " .. row)
return
end
local row_step_count = row_steps[row] or 16
print("DEBUG: Setting " .. row_step_count .. " steps to value " .. value .. " for row " .. row)
-- Set all steps to the specified value
for step = 1, row_step_count do
step_active[row][step] = true
step_data[row][step] = value
end
print("DEBUG: Set " .. row_step_count .. " steps, now updating canvas...")
-- Redraw canvas
if row_canvases[row] then
row_canvases[row]:update()
print("DEBUG: Canvas updated for row " .. row)
else
print("DEBUG: No canvas found for row " .. row)
end
-- Apply to automation immediately
print("DEBUG: Applying to automation...")
PakettiHyperEditWriteAutomationPattern(row)
renoise.app():show_status("HyperEdit Row " .. row .. ": Set all " .. row_step_count .. " steps to " .. value)
end
-- Change row step count and update UI
function PakettiHyperEditChangeRowStepCount(row, new_count)
row_steps[row] = new_count
-- Update the UI valuebox
if dialog_vb and dialog_vb.views["steps_" .. row] then
dialog_vb.views["steps_" .. row].value = new_count
end
-- Initialize new steps if needed
if not step_data[row] then
step_data[row] = {}
step_active[row] = {}
end
-- Clear existing steps beyond new count
for step = new_count + 1, MAX_STEPS do
step_active[row][step] = false
step_data[row][step] = 0.0
end
-- Redraw canvas with new step count
if row_canvases[row] then
row_canvases[row]:update()
end
renoise.app():show_status("HyperEdit Row " .. row .. ": Step count set to " .. new_count)
end
-- Fill row with pattern every N steps (like PakettiSliceEffectStepSequencer)
function PakettiHyperEditFillRowEveryN(row, interval)
if not row_parameters[row] then
renoise.app():show_status("HyperEdit Row " .. row .. ": Select parameter first")
return
end
local row_step_count = row_steps[row] or 16
-- Clear all steps first
for step = 1, MAX_STEPS do
if step_active[row] then
step_active[row][step] = false
end
if step_data[row] then
step_data[row][step] = 0.5 -- Default center value
end
end
-- Initialize arrays if needed
if not step_active[row] then step_active[row] = {} end
if not step_data[row] then step_data[row] = {} end
-- Fill every N steps (starting from step 1, so 1,1+N,1+2N...)
for step = 1, row_step_count, interval do
if step <= MAX_STEPS then
step_active[row][step] = true
step_data[row][step] = 0.5 -- Default center value
end
end
-- Update canvas and write pattern
if row_canvases[row] then
row_canvases[row]:update()
end
PakettiHyperEditWriteAutomationPattern(row)
renoise.app():show_status(string.format("HyperEdit Row %d: Filled every %d steps (%d total steps)", row, interval, row_step_count))
end
-- Stepsequencer state (MAX_STEPS already defined above as global)
-- Row count is now dynamic based on preferences
step_data = {} -- [row][step] = value (0.0 to 1.0) - Global
step_active = {} -- [row][step] = boolean - Global
-- Canvas dimensions per row - taller as requested
local canvas_width = 777
local canvas_height_per_row = 60 -- 2x taller (was 40)
local content_margin = 1
-- Mouse state
local mouse_is_down = false
local current_row_drawing = 0
local current_focused_row = 1 -- Track which row is currently focused for key operations
local mouse_state_monitor_timer = nil
local last_mouse_move_time = 0
-- Previous mouse position for interpolation (prevent bucktooth drawing)
local previous_mouse_step = {} -- [row] = step
local previous_mouse_value = {} -- [row] = normalized y value
-- Track switching state
local is_track_switching = false
-- Colors for visualization (GLOBAL so PakettiHyperEditUpdateColors can modify them)
COLOR_ACTIVE_STEP = {120, 40, 160, 255} -- Purple for active steps (will be updated by track color capture)
COLOR_INACTIVE_STEP = {40, 40, 40, 255} -- Dark gray for inactive steps
COLOR_GRID = {80, 80, 80, 255} -- Grid lines
COLOR_BACKGROUND = {20, 20, 20, 255} -- Dark background
-- Pre-configure parameters when opening on empty channel
function PakettiHyperEditPreConfigureParameters()
local song = renoise.song()
if not song then return end
local track = song.selected_track
if not track then return end
-- Check if there are any existing automations on this track - if so, skip pre-configuration
local current_pattern = song.selected_pattern_index
local track_index = song.selected_track_index
local pattern_track = song:pattern(current_pattern):track(track_index)
-- Quick check for any existing automations
local has_automation = false
for i = 2, #track.devices do -- Skip Track Vol/Pan
local device = track.devices[i]
for j = 1, #device.parameters do
local param = device.parameters[j]
if param.is_automatable then
local automation = pattern_track:find_automation(param)
if automation and #automation.points > 0 then
has_automation = true
break
end
end
end
if has_automation then break end
end
if has_automation then
-- If automation exists, populate from it instead of pre-configuring
print("DEBUG: Automation detected - populating from existing automation envelopes")
PakettiHyperEditPopulateFromExistingAutomation()
pre_configuration_applied = true -- Prevent init timer from running automation population again
return
end
-- Get available devices
local devices = PakettiHyperEditGetDevices()
if #devices == 0 then return end
-- Use priority system to find best instrument control device first, otherwise use first suitable device (avoid EQs, etc.)
local target_device_info = nil
local blacklisted_devices = {"Pro-Q", "FabFilter", "EQ", "Equalizer", "Filter", "Compressor"}
-- First try to find any priority instrument control device
local priority_device = PakettiHyperEditFindBestInstrControlDevice(track)
if priority_device then
-- Convert to device_info format expected by the rest of the code
for _, device_info in ipairs(devices) do
if device_info.track_index == song.selected_track_index and
device_info.device_index == priority_device.index then
target_device_info = device_info
print("DEBUG: Found " .. priority_device.device_type .. " device (priority " .. priority_device.priority .. ") - using preferred parameter order")
break
end
end
end
if not target_device_info then
-- Look for a suitable device (skip blacklisted ones)
for _, device_info in ipairs(devices) do
local is_blacklisted = false
for _, blacklisted in ipairs(blacklisted_devices) do
if device_info.name:find(blacklisted) then
is_blacklisted = true
print("DEBUG: Skipping blacklisted device: " .. device_info.name)
break
end
end
if not is_blacklisted then
target_device_info = device_info
print("DEBUG: Selected suitable device: " .. device_info.name)
break
end
end
end
if not target_device_info then
print("DEBUG: No suitable devices found - only blacklisted devices available, skipping pre-configuration")
return
end
local device_params = PakettiHyperEditGetParameters(target_device_info.device)
if #device_params == 0 then return end
-- Pre-configure rows with parameters using preferred order (NO automation case)
local max_params = 8
local max_rows = math.min(NUM_ROWS, max_params)
print("DEBUG: Pre-configuring " .. max_rows .. " rows using preferred parameter order (no existing automation)")
-- Use preferred order for all priority instrument control devices, or sequential for other devices
local preferred_order = nil
local priority_device_types = {"*Instr. MIDI Control", "*Instr. Automation", "*Instr. Macros"}
local is_priority_device = false
local is_instr_automation = false
for _, device_type in ipairs(priority_device_types) do
if target_device_info.name == device_type then
is_priority_device = true
if device_type == "*Instr. Automation" then
is_instr_automation = true
end
break
end
end
if is_priority_device then
if is_instr_automation or target_device_info.name == "*Instr. MIDI Control" then
-- For *Instr. Automation and *Instr. MIDI Control, use first 8 parameters directly instead of searching for specific names
preferred_order = nil -- Will use sequential assignment
print("DEBUG: Using " .. target_device_info.name .. " - will assign first 8 parameters sequentially")
else
-- Use the whitelist from DEVICE_PARAMETER_WHITELISTS for this device type (only *Instr. Macros has a hardcoded list)
preferred_order = DEVICE_PARAMETER_WHITELISTS[target_device_info.name]
if preferred_order and #preferred_order > 0 then
print("DEBUG: Using " .. target_device_info.name .. " preferred order: " .. table.concat(preferred_order, ", "))
else
print("DEBUG: No preferred order found for " .. target_device_info.name .. " - using sequential assignment")
preferred_order = nil
end
end
end
for row = 1, max_rows do
local param_info = nil
local param_index = nil
if preferred_order and row <= #preferred_order then
-- Use preferred order
local preferred_param_name = preferred_order[row]
print("DEBUG: Row " .. row .. " looking for preferred parameter: " .. preferred_param_name)
-- Find the parameter by name
for i, p in ipairs(device_params) do
if p.name == preferred_param_name then
param_info = p
param_index = i
print("DEBUG: Row " .. row .. " found preferred parameter: " .. preferred_param_name)
break
end
end
if not param_info then
print("DEBUG: Row " .. row .. " - preferred parameter '" .. preferred_param_name .. "' not found, skipping")
end
else
-- Fallback to sequential assignment for non-priority devices or when preferred list is exhausted
param_index = row + (is_priority_device and 1 or 0)
if param_index <= #device_params then
param_info = device_params[param_index]
print("DEBUG: Row " .. row .. " using sequential parameter: " .. (PakettiHyperEditCleanParameterName(param_info.name) or "unknown"))
end
end
if not param_info then
print("DEBUG: Row " .. row .. " - no suitable parameter found, stopping pre-configuration")
break
end
-- If we encounter X_PitchBend, look for Pitchbend instead
if param_info.name == "X_PitchBend" then
for i, p in ipairs(device_params) do
if p.name == "Pitchbend" then
param_info = p
param_index = i
break
end
end
end
-- Set device for this row (store AudioDevice directly for consistency)
row_devices[row] = target_device_info.device
parameter_lists[row] = device_params
-- Set parameter for this row
row_parameters[row] = param_info
-- Auto-populate pitchbend parameters with 0.5 values when dialog opens
local param_name = param_info.name:lower()
if param_name:find("pitchbend") or param_name:find("x_pitchbend") then
print("DEBUG: Pre-configuring Pitchbend parameter for row " .. row .. " - auto-populating with 0.5 values")
PakettiHyperEditSetAllStepsToValue(row, 0.5)
end
-- Update UI elements if they exist
if dialog_vb then
-- Find the correct device index for target_device_info
local target_device_index = 1 -- fallback
for i, device_info in ipairs(devices) do
if device_info.name == target_device_info.name then
target_device_index = i
print("DEBUG: Found target device " .. target_device_info.name .. " at index " .. i)
break
end
end
-- Update device popup (set to target device)
local device_popup = dialog_vb.views["device_popup_" .. row]
if device_popup then
print("DEBUG: Pre-config setting device popup for row " .. row .. " to index " .. target_device_index .. " (" .. target_device_info.name .. ")")
device_popup.value = target_device_index
end
-- Update parameter popup
local param_popup = dialog_vb.views["parameter_popup_" .. row]
if param_popup then
local param_names = {}
for _, p in ipairs(device_params) do
table.insert(param_names, PakettiHyperEditCleanParameterName(p.name))
end
param_popup.items = param_names
if param_index then
param_popup.value = param_index -- Select the correct parameter index
end
end
end
end
-- Set flag to indicate pre-configuration was applied
pre_configuration_applied = true
renoise.app():show_status("HyperEdit: Pre-configured first " .. max_rows .. " rows with " .. target_device_info.name .. " parameters")
end
-- Initialize step data for all rows
function PakettiHyperEditInitStepData()
step_data = {}
step_active = {}
row_steps = {}
for row = 1, NUM_ROWS do
step_data[row] = {}
step_active[row] = {}
row_steps[row] = 16 -- Default 16 steps per row
for step = 1, MAX_STEPS do -- Initialize for max steps
step_data[row][step] = 0.5 -- Default to middle value
step_active[row][step] = false -- Default to inactive
end
end
end
-- Get available devices from current track (skip Track Vol/Pan)
function PakettiHyperEditGetDevices()
local song = renoise.song()
if not song then return {} end
local track = song.selected_track
if not track then return {} end
local devices = {}
-- Skip the first device (Track Vol/Pan) and start with device 2
for i = 2, #track.devices do
local device = track.devices[i]
table.insert(devices, {
index = i,
device = device,
name = device.display_name or ("Device " .. i)
})
end
return devices
end
-- Get automatable parameters from device (with optional whitelist filtering)
function PakettiHyperEditGetParameters(device)
if not device then return {} end
local all_params = {}
-- Get all automatable parameters
for i = 1, #device.parameters do
local param = device.parameters[i]
if param.is_automatable then
-- UNIVERSAL FILTER: Skip X_PitchBend parameters entirely - they should never be shown
if param.name == "X_PitchBend" then
-- Skip this parameter completely
print("DEBUG: Filtered out X_PitchBend parameter - should never be displayed")
else
-- Special handling for Gainer device - custom range mapping
local device_name = device.display_name or ""
local param_min = param.value_min
local param_max = param.value_max
local param_default = param.value_default
if device_name == "Gainer" and param.name == "Gain" then
-- Custom mapping: HyperEdit 0.0-1.0 maps to Renoise 0.0-1.0 (not 0.0-4.0)
param_min = 0.0
param_max = 1.0
param_default = 1.0
print("DEBUG: Gainer Gain parameter - using custom range 0.0-1.0 (instead of " .. param.value_min .. "-" .. param.value_max .. ")")
end
table.insert(all_params, {
index = i,
parameter = param,
name = param.name,
value_min = param_min,
value_max = param_max,
value_default = param_default,
-- Store original parameter info for custom mapping
original_min = param.value_min,
original_max = param.value_max,
is_custom_mapped = (device_name == "Gainer" and param.name == "Gain")
})
end
end
end
-- Check if this device has a whitelist
local device_name = device.display_name or ""
local whitelist = DEVICE_PARAMETER_WHITELISTS[device_name]
if not whitelist or #whitelist == 0 then
-- No whitelist or empty whitelist - return all parameters
print("DEBUG: No whitelist or empty whitelist for device: " .. device_name .. " - showing all " .. #all_params .. " parameters")
return all_params
end
-- Apply whitelist filtering in PREFERRED ORDER
local filtered_params = {}
-- Add parameters in whitelist order (preserves preferred order)
for _, whitelisted_name in ipairs(whitelist) do
for _, param_info in ipairs(all_params) do
if param_info.name == whitelisted_name then
table.insert(filtered_params, param_info)
break -- Found this parameter, move to next in whitelist
end
end
end
print("DEBUG: Applied whitelist for " .. device_name .. " - filtered from " .. #all_params .. " to " .. #filtered_params .. " parameters")
return filtered_params
end
-- Update all device lists
function PakettiHyperEditUpdateAllDeviceLists()
if not hyperedit_dialog or not hyperedit_dialog.visible then
return
end
-- Set flag to prevent auto-parameter assignment during device updates
is_updating_device_lists = true
print("DEBUG: Device change detected - updating dropdowns while preserving assignments")
local new_devices = PakettiHyperEditGetDevices()
local device_names = {}
for i, device_info in ipairs(new_devices) do
table.insert(device_names, device_info.name)
end
if #device_names == 0 then
device_names = {"No devices available"}
end
print("DEBUG: Updating device dropdowns with " .. #new_devices .. " devices")
print("DEBUG: NUM_ROWS = " .. NUM_ROWS .. ", processing rows 1 to " .. NUM_ROWS)
-- Update dropdown items and carefully preserve existing assignments
local preserved_count = 0
local updated_count = 0
for row = 1, NUM_ROWS do
local device_popup = dialog_vb and dialog_vb.views["device_popup_" .. row]
if device_popup then
-- Save current assignment info BEFORE touching the dropdown
local current_device = row_devices[row]
local current_parameter = row_parameters[row]
print("DEBUG: Processing row " .. row .. " - has_device: " .. (current_device and "YES" or "NO"))
-- Update the items list (this is safe)
device_popup.items = device_names
updated_count = updated_count + 1
-- If this row has an existing device assignment, find its new index
if current_device then
local found_index = nil
for i, device_info in ipairs(new_devices) do
if device_info.device.display_name == current_device.display_name then
found_index = i
break
end
end