-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathPakettiCanvasExperiments.lua
More file actions
2903 lines (2457 loc) · 109 KB
/
PakettiCanvasExperiments.lua
File metadata and controls
2903 lines (2457 loc) · 109 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
-- PakettiCanvasExperiments.lua
-- Canvas-based Device Parameter Editor
-- Allows visual editing of device parameters through a canvas interface
local vb = renoise.ViewBuilder()
-- Base canvas dimensions (actual size calculated at dialog creation time)
local base_canvas_width = 1280 -- keep wide like EQ30
local base_canvas_height = 390 -- reduced to take less vertical space (match EQ30)
local canvas_width = base_canvas_width -- Will be updated in dialog creation
local canvas_height = base_canvas_height -- Will be updated in dialog creation
local content_margin = 3 -- tighter margin around the content area
local content_width = canvas_width - (content_margin * 2) -- Will be updated in dialog creation
local content_height = canvas_height - (content_margin * 2) -- Will be updated in dialog creation
local content_x = content_margin
local content_y = content_margin
local canvas_experiments_dialog = nil
local canvas_experiments_canvas = nil
local current_device = nil
local device_parameters = {}
local parameter_width = 0
local mouse_is_down = false
-- Add variables for drawing feedback
local last_mouse_x = -1
local last_mouse_y = -1
-- Device selection observer
local device_selection_notifier = nil
-- Song change observer
local song_change_notifier = nil
-- Dynamic status text view
local status_text_view = nil
-- Current drawing parameter info
local current_drawing_parameter = nil
-- Track information for current device
local current_track_index = nil
local current_track_name = nil
-- Global device selection observer for auto-open feature
local global_device_observer = nil
-- Edit A/B functionality (inspired by PakettiPCMWriter)
local current_edit_mode = "A" -- "A" or "B"
local parameter_values_A = {} -- Store parameter values for Edit A
local parameter_values_B = {} -- Store parameter values for Edit B
local crossfade_amount = 0.0 -- 0.0 = full A, 1.0 = full B
-- Follow pattern automation parameter writing (DEFAULT: OFF)
local follow_automation = false
local device_parameter_observers = {}
-- Auto-switch to automation view when enabling automation sync (DEFAULT: OFF)
local auto_show_automation = false
-- Expose parameters on mixer when drawing with automation sync (DEFAULT: OFF)
local expose_params_on_mixer = false
-- Clean parameter names by removing "CC XX " prefix (DEFAULT: ON)
local clean_parameter_names = true
-- Track which specific parameter is being manually edited (automation sync aware)
local parameter_being_drawn = nil -- Index of parameter currently being drawn
local automation_reading_enabled = true
-- Track initialization cooldown to prevent auto-open during track/device creation
local last_device_change_time = 0
local device_change_cooldown_ms = 500 -- 500ms cooldown to avoid opening during batch device adds
-- Button colors for Edit A/B
local COLOR_BUTTON_ACTIVE = {0xFF, 0x80, 0x80} -- Light red for active
local COLOR_BUTTON_INACTIVE = {0x80, 0x80, 0x80} -- Gray for inactive
-- Canvas bar colors for Edit A/B visualization (consistent like PakettiPCMWriter)
local COLOR_EDIT_A_ACTIVE = {120, 40, 160, 255} -- Purple for Edit A (bold when editing A)
local COLOR_EDIT_A_INACTIVE = {120, 40, 160, 180} -- Purple for Edit A (faded when not editing A)
local COLOR_EDIT_B_ACTIVE = {160, 160, 40, 255} -- Yellow for Edit B (bold when editing B)
local COLOR_EDIT_B_INACTIVE = {160, 160, 40, 180} -- Yellow for Edit B (faded when not editing B)
local COLOR_CROSSFADE = {80, 160, 40, 255} -- Green for crossfade outline
-- Canvas update timer
local canvas_update_timer = nil
-- Remember previous device index for smart restoration
local previous_device_index = nil
-- Performance throttling variables
local status_update_throttle_ms = 100 -- Update status text less frequently
local last_status_update_time = 0
local canvas_refresh_rate = 1 -- Default 1ms, user-configurable (fastest)
local canvas_refresh_options = {1, 5, 10, 25, 50, 100} -- Dropdown options in ms
-- Helper function to get UI width (no longer scaled by half-size preference)
local function get_ui_width(base_width)
return base_width
end
-- Helper function to clean parameter names by removing "CC XX " prefix
local function get_clean_parameter_name(param_name)
if not clean_parameter_names or 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
-- Navigation functions for track and device switching
function PakettiCanvasExperimentsSelectPreviousTrack()
local song = renoise.song()
local current_track = song.selected_track_index
local new_track = current_track - 1
if new_track < 1 then
new_track = #song.tracks
end
song.selected_track_index = new_track
renoise.app():show_status("Switched to track: " .. song.tracks[new_track].name)
end
function PakettiCanvasExperimentsSelectNextTrack()
local song = renoise.song()
local current_track = song.selected_track_index
local new_track = current_track + 1
if new_track > #song.tracks then
new_track = 1
end
song.selected_track_index = new_track
renoise.app():show_status("Switched to track: " .. song.tracks[new_track].name)
end
function PakettiCanvasExperimentsSelectPreviousDevice()
local song = renoise.song()
local track = song.selected_track
if #track.devices > 0 then
local current_device = song.selected_device_index or 1
local new_device = current_device - 1
if new_device < 1 then
new_device = #track.devices
end
song.selected_device_index = new_device
local device_name = track.devices[new_device].display_name or "Unknown"
renoise.app():show_status("Switched to device: " .. device_name)
else
renoise.app():show_status("No devices available on this track")
end
end
function PakettiCanvasExperimentsSelectNextDevice()
local song = renoise.song()
local track = song.selected_track
if #track.devices > 0 then
local current_device = song.selected_device_index or 1
local new_device = current_device + 1
if new_device > #track.devices then
new_device = 1
end
song.selected_device_index = new_device
local device_name = track.devices[new_device].display_name or "Unknown"
renoise.app():show_status("Switched to device: " .. device_name)
else
renoise.app():show_status("No devices available on this track")
end
end
-- Function to detect automation frame and selected parameter
function PakettiCanvasExperimentsDetectAutomationSelection()
local song = renoise.song()
-- Check if automation frame is displayed
local automation_frame_active = (renoise.app().window.active_lower_frame == renoise.ApplicationWindow.LOWER_FRAME_TRACK_AUTOMATION)
if automation_frame_active then
-- Check if there's a selected automation parameter and device
local selected_automation_param = song.selected_automation_parameter
local selected_automation_device = song.selected_automation_device
if selected_automation_param and selected_automation_param.is_automatable and selected_automation_device then
print("AUTOMATION_DETECTION: Automation frame active, parameter: " .. selected_automation_param.name)
print("AUTOMATION_DETECTION: Device: " .. selected_automation_device.display_name)
-- Find the device index in the current track using display_name comparison
local current_track = song.selected_track
for device_index, device in ipairs(current_track.devices) do
if device.display_name == selected_automation_device.display_name then
print("AUTOMATION_DETECTION: Found device at index " .. device_index .. ": " .. device.display_name)
-- Select this device
song.selected_device_index = device_index
renoise.app():show_status("Auto-selected device from automation: " .. device.display_name .. " (" .. selected_automation_param.name .. ")")
return true
end
end
print("AUTOMATION_DETECTION: Device not found in current track devices")
end
end
return false
end
-- Randomization strength slider
local randomize_strength = 50 -- Default 50%
local randomize_slider_view = nil
-- Shared canvas font (moved to PakettiCanvasFont.lua)
-- Use PakettiCanvasFontLetterFunctions for per-character drawing and PakettiCanvasFontDrawText for strings
-- Function to toggle clean parameter names (silent, no UI button)
function PakettiCanvasExperimentsToggleCleanNames()
clean_parameter_names = not clean_parameter_names
-- Update canvas to show cleaned/original names
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
-- Update status text
if status_text_view then
status_text_view.text = PakettiCanvasExperimentsGetStatusText()
end
local status = clean_parameter_names and "ON" or "OFF"
renoise.app():show_status("Parameter name cleaning: " .. status)
end
-- Generate dynamic status text
function PakettiCanvasExperimentsGetStatusText()
-- Safe nil checking throughout with proper error handling
local success, result = pcall(function()
if not current_device then
return "No device selected"
end
local song = renoise.song()
if not song then
return "No song available"
end
local device_name = "Unknown Device"
if current_device and current_device.short_name then
device_name = current_device.short_name
elseif current_device and current_device.display_name then
device_name = current_device.display_name
end
local param_count = 0
if device_parameters then
param_count = #device_parameters
end
local base_text = string.format("%s (%d)", device_name, param_count)
-- Safe current parameter access with multiple nil checks
if current_drawing_parameter and
current_drawing_parameter.parameter and
current_drawing_parameter.parameter.value ~= nil and
current_drawing_parameter.name and
current_drawing_parameter.index then
local param_info = current_drawing_parameter
local param_text = string.format(": %s = %.3f",
get_clean_parameter_name(param_info.name), param_info.parameter.value)
return base_text .. param_text
end
return base_text
end)
-- Return result if successful, otherwise fallback
if success then
return result
else
return "Canvas Parameter Editor - Error accessing device state"
end
end
-- Refresh device parameters when device selection changes
function PakettiCanvasExperimentsRefreshDevice()
-- CRITICAL: Only run if dialog is still open
if not canvas_experiments_dialog or not canvas_experiments_dialog.visible then
return
end
local song = renoise.song()
-- Check if selected device is Pro-Q 3 - if so, close Parameter Editor and open external editor
if song and song.selected_device and song.selected_device.display_name then
local device = song.selected_device
local device_name = device.display_name
if device_name:find("Pro%-Q 3") then
print("DEVICE_SWITCH: Selected Pro-Q 3, closing Parameter Editor and opening external editor")
-- Close Parameter Editor
PakettiCanvasExperimentsCleanup()
if canvas_experiments_dialog then
canvas_experiments_dialog:close()
canvas_experiments_dialog = nil
end
-- Open external editor
if device.external_editor_available then
device.external_editor_visible = true
end
return
end
-- Check if selected device is EQ30/EQ64 - if so, close Parameter Editor and open EQ30 dialog instead
if device_name:match("^EQ30 Device [1-4]$") or device_name:match("^EQ64 Device [1-8]$") then
print("DEVICE_SWITCH: Selected EQ30/EQ64 device '" .. device_name .. "', closing Parameter Editor and opening EQ30 dialog")
-- Close Parameter Editor
PakettiCanvasExperimentsCleanup()
if canvas_experiments_dialog then
canvas_experiments_dialog:close()
canvas_experiments_dialog = nil
end
-- Open EQ30 dialog
PakettiEQ30ShowAndFollow()
return
end
end
print("=== Device Selection Changed ===")
-- Reset A/B state when device changes (but preserve automation sync setting)
parameter_values_A = {}
parameter_values_B = {}
crossfade_amount = 0.0
current_edit_mode = "A"
-- Keep follow_automation setting when switching devices
-- Clear any manual editing state
parameter_being_drawn = nil
automation_reading_enabled = true
-- Update UI to reflect reset state
if vb.views.edit_a_button then
vb.views.edit_a_button.color = COLOR_BUTTON_ACTIVE
end
if vb.views.edit_b_button then
vb.views.edit_b_button.color = COLOR_BUTTON_INACTIVE
end
if vb.views.crossfade_slider then
vb.views.crossfade_slider.value = 0.0
end
if vb.views.randomize_button then
vb.views.randomize_button.text = "Randomize Edit A"
end
if vb.views.follow_automation_button then
-- Update button to reflect actual follow_automation state
vb.views.follow_automation_button.text = follow_automation and "Automation Sync: ON" or "Automation Sync: OFF"
vb.views.follow_automation_button.color = follow_automation and {0, 120, 0} or {96, 96, 96}
end
-- Keep Renoise's follow player setting consistent with automation sync
song.transport.follow_player = follow_automation
-- Remove parameter observers for old device
RemoveParameterObservers()
-- Remember the current device index before it potentially gets lost
if song.selected_device_index then
previous_device_index = song.selected_device_index
end
-- Clear current drawing state when device changes
current_drawing_parameter = nil
mouse_is_down = false
last_mouse_x = -1
last_mouse_y = -1
-- Check if we have a selected device
local selected_device = nil
if song and song.selected_device then
selected_device = song.selected_device
end
if not selected_device then
print("DEVICE_ERROR: No device selected - trying to restore previous device position")
-- Try to restore to previous device position if we remember it
local found_device = nil
if previous_device_index then
local current_track = song.selected_track
if current_track and #current_track.devices > 0 then
-- Smart device selection: try to stay close to where we were
local target_device_index
if previous_device_index <= #current_track.devices then
-- Previous index still exists, use it
target_device_index = previous_device_index
else
-- Previous index is too high, go to the last available device (not device 1!)
target_device_index = #current_track.devices
end
song.selected_device_index = target_device_index
found_device = song.selected_device
selected_device = found_device
end
end
-- If restoration failed, DON'T force track changes - just work with current track
if not found_device then
print("DEVICE_ERROR: No device found - working with current track state without forcing changes")
-- Don't force track selection changes - let user control track selection
selected_device = nil
end
-- If no devices found at all, then show "no device selected"
if not found_device then
print("DEVICE_ERROR: No devices found in entire song")
current_device = nil
device_parameters = {}
parameter_width = 0
selected_device = nil
-- Update status text
if status_text_view then
status_text_view.text = PakettiCanvasExperimentsGetStatusText()
end
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
-- Show status message but continue - don't return early
renoise.app():show_status("No devices found - add a device to continue")
end
end
if selected_device then
print("DEVICE_OK: New selected device:")
print(" Device name: " .. (selected_device.display_name or "Unknown"))
print(" Total parameters: " .. #selected_device.parameters)
current_device = selected_device
device_parameters = {}
-- Check if this is a Wavetable Mod *LFO device (partial blacklist)
local is_wavetable_lfo = false
local device_name = selected_device.display_name or ""
local actual_device_name = selected_device.name or ""
-- Check for Wavetable Mod *LFO by display name OR by placeholder name OR by device type
if device_name == "Wavetable Mod *LFO" or
device_name == "PAKETTI_PLACEHOLDER_001" or
(actual_device_name == "*LFO" and #selected_device.parameters == 8) then
is_wavetable_lfo = true
print("DEVICE_INFO: Wavetable Mod *LFO detected - hiding first 3 routing parameters")
end
-- Get all automatable parameters from the device
for i = 1, #current_device.parameters do
local param = current_device.parameters[i]
print(" Parameter " .. i .. ": " .. param.name .. " (automatable: " .. tostring(param.is_automatable) .. ")")
-- Skip first 3 parameters for Wavetable Mod *LFO devices
local should_skip = is_wavetable_lfo and (i <= 3)
if param.is_automatable and not should_skip then
print(" Value: " .. param.value .. " (min: " .. param.value_min .. ", max: " .. param.value_max .. ", default: " .. param.value_default .. ")")
table.insert(device_parameters, {
parameter = param,
name = param.name,
value = param.value,
value_min = param.value_min,
value_max = param.value_max,
value_default = param.value_default,
index = i
})
elseif should_skip then
print(" SKIPPED: Parameter hidden for Wavetable Mod *LFO")
end
end
else
print("DEVICE_WARNING: selected_device is nil after search - using empty state")
current_device = nil
device_parameters = {}
end
if #device_parameters == 0 then
parameter_width = 0
else
-- Calculate parameter width based on content width
parameter_width = content_width / #device_parameters
-- Automatically capture current device parameters to Edit A
for i, param_info in ipairs(device_parameters) do
parameter_values_A[i] = param_info.parameter.value
end
print("DEVICE_CHANGE: Captured " .. #device_parameters .. " current device parameters to Edit A")
end
-- Update status text
if status_text_view then
status_text_view.text = PakettiCanvasExperimentsGetStatusText()
end
-- Update canvas if it exists
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
-- Setup parameter observers for automation visualization (always setup for canvas updates)
SetupParameterObservers()
-- Force immediate canvas update after device change
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
-- Show status message
renoise.app():show_status(PakettiCanvasExperimentsGetStatusText())
end
-- Initialize the canvas experiments
function PakettiCanvasExperimentsInit()
-- If dialog is already open, just return - let the device selection observer handle the device change
if canvas_experiments_dialog and canvas_experiments_dialog.visible then
return
end
local song = renoise.song()
-- First, try to detect automation frame and auto-select device if applicable
local automation_detected = PakettiCanvasExperimentsDetectAutomationSelection()
-- Check if we have a selected device with safe access
local selected_device = nil
if song and song.selected_device then
selected_device = song.selected_device
end
if not selected_device then
print("DEVICE_ERROR: No device selected - searching for available devices")
-- Don't force track changes - work with current track state
print("INIT: No device selected - working with current track without forcing changes")
selected_device = nil
if not selected_device then
print("INIT: No device available - dialog will open with empty state")
-- Continue anyway - let the dialog open with no device state
current_device = nil
device_parameters = {}
parameter_width = 0
end
end
if selected_device then
print("DEVICE_OK: Selected device found:")
print(" Device name: " .. (selected_device.display_name or "Unknown"))
print(" Total parameters: " .. #selected_device.parameters)
current_device = selected_device
device_parameters = {}
-- Check if this is a Wavetable Mod *LFO device (partial blacklist)
local is_wavetable_lfo = false
local device_name = selected_device.display_name or ""
local actual_device_name = selected_device.name or ""
-- Check for Wavetable Mod *LFO by display name OR by placeholder name OR by device type
if device_name == "Wavetable Mod *LFO" or
device_name == "PAKETTI_PLACEHOLDER_001" or
(actual_device_name == "*LFO" and #selected_device.parameters == 8) then
is_wavetable_lfo = true
print("DEVICE_INFO: Wavetable Mod *LFO detected - hiding first 3 routing parameters")
end
-- Get all automatable parameters from the device
for i = 1, #current_device.parameters do
local param = current_device.parameters[i]
print(" Parameter " .. i .. ": " .. param.name .. " (automatable: " .. tostring(param.is_automatable) .. ")")
-- Skip first 3 parameters for Wavetable Mod *LFO devices
local should_skip = is_wavetable_lfo and (i <= 3)
if param.is_automatable and not should_skip then
print(" Value: " .. param.value .. " (min: " .. param.value_min .. ", max: " .. param.value_max .. ", default: " .. param.value_default .. ")")
table.insert(device_parameters, {
parameter = param,
name = param.name,
value = param.value,
value_min = param.value_min,
value_max = param.value_max,
value_default = param.value_default,
index = i
})
elseif should_skip then
print(" SKIPPED: Parameter hidden for Wavetable Mod *LFO")
end
end
else
print("DEVICE_ERROR: No valid device available - initializing with empty state")
current_device = nil
device_parameters = {}
end
print("DEVICE_INFO: Found " .. #device_parameters .. " automatable parameters")
if #device_parameters == 0 then
print("DEVICE_INFO: No automatable parameters - dialog will show empty canvas")
parameter_width = 0
else
-- Calculate parameter width based on content width
parameter_width = content_width / #device_parameters
print("DEVICE_INFO: Parameter width: " .. parameter_width .. " pixels each")
end
-- Automatically capture current device parameters to Edit A
if #device_parameters > 0 then
for i, param_info in ipairs(device_parameters) do
parameter_values_A[i] = param_info.parameter.value
end
print("INIT: Captured " .. #device_parameters .. " current device parameters to Edit A")
end
-- Create the dialog
PakettiCanvasExperimentsCreateDialog()
-- Set up device selection observer
if device_selection_notifier then
pcall(function()
if song and song.selected_device_observable then
song.selected_device_observable:remove_notifier(device_selection_notifier)
print("INIT: Removed existing device selection observer")
end
end)
device_selection_notifier = nil
end
device_selection_notifier = function()
PakettiCanvasExperimentsRefreshDevice()
end
song.selected_device_observable:add_notifier(device_selection_notifier)
-- Setup app new document observer to handle new songs
if song_change_notifier then
pcall(function()
if renoise.tool().app_new_document_observable:has_notifier(song_change_notifier) then
renoise.tool().app_new_document_observable:remove_notifier(song_change_notifier)
print("INIT: Removed existing app new document observer")
end
end)
song_change_notifier = nil
end
song_change_notifier = function()
print("SONG_CHANGE: New document loaded - refreshing device state")
PakettiCanvasExperimentsRefreshDevice()
end
renoise.tool().app_new_document_observable:add_notifier(song_change_notifier)
end
-- Handle mouse input
function PakettiCanvasExperimentsHandleMouse(ev)
-- print("DEBUG: Mouse event - type: " .. ev.type .. ", position: " .. ev.position.x .. ", " .. ev.position.y)
local w = canvas_width
local h = canvas_height
-- Handle mouse leave event - but don't stop dragging!
if ev.type == "exit" then
-- print("DEBUG: Mouse exit event - keeping mouse_is_down state")
-- Don't reset mouse_is_down here - let user come back and continue dragging
return
end
-- Check if mouse is within canvas bounds
local mouse_in_canvas = ev.position.x >= 0 and ev.position.x < w and
ev.position.y >= 0 and ev.position.y < h
-- Check if mouse is within content area bounds
local mouse_in_content = ev.position.x >= content_x and ev.position.x < (content_x + content_width) and
ev.position.y >= content_y and ev.position.y < (content_y + content_height)
-- print("DEBUG: Mouse in canvas: " .. tostring(mouse_in_canvas) .. ", in content: " .. tostring(mouse_in_content))
-- Always handle mouse events if mouse is in canvas, regardless of content area
if not mouse_in_canvas then
-- Only handle mouse up when outside canvas to ensure we can stop dragging
if ev.type == "up" then
mouse_is_down = false
-- RE-ENABLE ONLY THE SPECIFIC PARAMETER'S OBSERVER (if it was disabled)
if follow_automation and parameter_being_drawn then
local param_info = device_parameters[parameter_being_drawn]
if param_info and param_info.parameter and param_info.parameter.value_observable then
local parameter = param_info.parameter
local observer = device_parameter_observers[parameter]
if observer then
if not parameter.value_observable:has_notifier(observer) then
parameter.value_observable:add_notifier(observer)
end
end
end
end
-- Clear parameter being drawn
parameter_being_drawn = nil
last_mouse_x = -1
last_mouse_y = -1
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
if status_text_view then
status_text_view.text = PakettiCanvasExperimentsGetStatusText()
end
end
return
end
local x = ev.position.x
local y = ev.position.y
-- Always update mouse tracking for cursor display
last_mouse_x = x
last_mouse_y = y
if ev.type == "down" then
mouse_is_down = true
-- Calculate which parameter we're starting to draw on
if mouse_in_content and current_device and device_parameters and #device_parameters > 0 and parameter_width > 0 then
parameter_being_drawn = math.floor((x - content_x) / parameter_width) + 1
parameter_being_drawn = math.max(1, math.min(parameter_being_drawn, #device_parameters))
-- If automation sync is ON, disable ONLY this parameter's observer to prevent jitter
if follow_automation then
local param_info = device_parameters[parameter_being_drawn]
if param_info and param_info.parameter then
local parameter = param_info.parameter
local observer = device_parameter_observers[parameter]
-- Disable automation observer for this parameter to prevent drawing conflicts
if observer then
if parameter.value_observable:has_notifier(observer) then
parameter.value_observable:remove_notifier(observer)
renoise.app():show_status("Drawing: Automation disabled for " .. param_info.name)
end
end
end
end
end
-- Only apply parameter changes if we're in the content area
if mouse_in_content then
PakettiCanvasExperimentsHandleMouseInput(x, y)
else
current_drawing_parameter = nil
end
elseif ev.type == "up" then
mouse_is_down = false
-- RE-ENABLE ONLY THE SPECIFIC PARAMETER'S OBSERVER (if it was disabled)
if follow_automation and parameter_being_drawn then
local param_info = device_parameters[parameter_being_drawn]
if param_info and param_info.parameter and param_info.parameter.value_observable then
local parameter = param_info.parameter
local observer = device_parameter_observers[parameter]
if observer then
if not parameter.value_observable:has_notifier(observer) then
parameter.value_observable:add_notifier(observer)
renoise.app():show_status("Drawing complete: Automation resumed for " .. param_info.name)
end
else
-- SAFETY: Rebuild the observer if it was lost (e.g., due to remove_all_notifiers)
local param_index = parameter_being_drawn -- Capture in closure
local new_observer = function()
if not canvas_experiments_dialog or not canvas_experiments_dialog.visible then
return
end
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
end
parameter.value_observable:add_notifier(new_observer)
device_parameter_observers[parameter] = new_observer
renoise.app():show_status("Drawing complete: Automation observer rebuilt for " .. param_info.name)
end
end
end
-- Clear parameter being drawn
parameter_being_drawn = nil
-- Clear mouse tracking and update canvas immediately to hide cursor
last_mouse_x = -1
last_mouse_y = -1
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
-- Update status text
if status_text_view then
status_text_view.text = PakettiCanvasExperimentsGetStatusText()
end
elseif ev.type == "move" then
if mouse_is_down then
-- Update immediately on every mouse move for maximum responsiveness
if mouse_in_content then
PakettiCanvasExperimentsHandleMouseInput(x, y)
else
-- Keep current_drawing_parameter visible even when outside content area
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
end
end
end
end
-- Handle mouse input for parameter editing (only called when in content area)
function PakettiCanvasExperimentsHandleMouseInput(x, y)
-- print("DEBUG: Mouse input at " .. x .. ", " .. y .. " (content area)")
if not current_device or not device_parameters or #device_parameters == 0 then
-- print("DEBUG: No device or parameters available for mouse input")
current_drawing_parameter = nil
return
end
-- Calculate which parameter column we're in (relative to content area)
local parameter_index = 1
if parameter_width > 0 then
parameter_index = math.floor((x - content_x) / parameter_width) + 1
parameter_index = math.max(1, math.min(parameter_index, #device_parameters))
end
-- print("DEBUG: Drawing on parameter " .. parameter_index .. " (" .. device_parameters[parameter_index].name .. ")")
-- Calculate normalized Y position (0 = max, 1 = min) relative to content area
local normalized_y = 1.0 - ((y - content_y) / content_height)
normalized_y = math.max(0, math.min(1, normalized_y))
-- print("DEBUG: Normalized Y: " .. normalized_y)
-- Update the parameter we're currently touching
local param_info = device_parameters[parameter_index]
if param_info then
-- Set current drawing parameter for status display
current_drawing_parameter = param_info
local new_value = param_info.value_min + (normalized_y * (param_info.value_max - param_info.value_min))
-- print("DEBUG: Setting parameter " .. parameter_index .. " (" .. param_info.name .. ") to " .. new_value)
if current_edit_mode == "A" then
-- Edit A mode: modify device parameters directly (Edit A IS the device)
param_info.parameter.value = new_value
-- Write to automation if following is enabled
if follow_automation then
WriteParameterToAutomation(param_info.parameter, new_value, false) -- Show envelope for manual drawing
-- Expose parameter on mixer if option is enabled
if expose_params_on_mixer then
pcall(function()
param_info.parameter.show_in_mixer = true
end)
end
end
elseif current_edit_mode == "B" then
-- Edit B mode: modify Edit B values only, device unchanged until crossfade
parameter_values_B[parameter_index] = new_value
-- print("DEBUG: Stored Edit B value for parameter " .. parameter_index .. ": " .. new_value)
end
-- Throttle status text updates for performance
local current_time = os.clock() * 1000
if current_time - last_status_update_time >= status_update_throttle_ms then
last_status_update_time = current_time
if status_text_view then
status_text_view.text = PakettiCanvasExperimentsGetStatusText()
end
end
-- Update canvas IMMEDIATELY for responsive drawing feedback
if canvas_experiments_canvas then
canvas_experiments_canvas:update()
end
else
-- print("DEBUG: No parameter info found for index " .. parameter_index)
current_drawing_parameter = nil
end
end
-- Draw the canvas
function PakettiCanvasExperimentsDrawCanvas(ctx)
local w, h = canvas_width, canvas_height
-- Use the exact same clear pattern as working PCMWriter
ctx:clear_rect(0, 0, w, h)
-- CRITICAL: Check if dialog is still open and valid
if not canvas_experiments_dialog or not canvas_experiments_dialog.visible then
return
end
-- CRITICAL: Check if current device is still valid
if not current_device then
-- Draw "no device" message
ctx.stroke_color = {128, 128, 128, 255} -- Gray - friendly message
ctx.line_width = 2
local center_x = w / 2
local center_y = h / 2
-- Draw "No Device Selected" text using simple lines
ctx:begin_path()
ctx:move_to(center_x - 80, center_y)
ctx:line_to(center_x + 80, center_y)
ctx:stroke()
ctx:begin_path()
ctx:move_to(center_x, center_y - 20)
ctx:line_to(center_x, center_y + 20)
ctx:stroke()
return
end
-- print("DEBUG: Canvas size: " .. w .. "x" .. h)
-- print("DEBUG: Content area: " .. content_width .. "x" .. content_height .. " at " .. content_x .. "," .. content_y)
-- print("DEBUG: Device parameters count: " .. #device_parameters)
if #device_parameters == 0 then
-- Draw "no parameters" message
ctx.stroke_color = {128, 128, 128, 255} -- Gray - friendly message
ctx.line_width = 2
-- Draw a simple message in the center
local center_x = w / 2
local center_y = h / 2
-- Draw "No Device Parameters" text using simple lines
ctx:begin_path()
ctx:move_to(center_x - 100, center_y)
ctx:line_to(center_x + 100, center_y)
ctx:stroke()
ctx:begin_path()
ctx:move_to(center_x, center_y - 20)
ctx:line_to(center_x, center_y + 20)
ctx:stroke()
return
end
-- Draw background grid within content area only
ctx.stroke_color = {32, 0, 48, 255} -- Dark purple grid - using 0-255 integers
ctx.line_width = 1
for i = 0, 10 do
local x = content_x + (i / 10) * content_width
ctx:begin_path()
ctx:move_to(x, content_y)
ctx:line_to(x, content_y + content_height)
ctx:stroke()
end
for i = 0, 10 do
local y = content_y + (i / 10) * content_height
ctx:begin_path()
ctx:move_to(content_x, y)
ctx:line_to(content_x + content_width, y)
ctx:stroke()
end
-- Draw center line within content area (like zero line in PCMWriter)
ctx.stroke_color = {128, 128, 128, 255} -- Gray center line - using 0-255 integers
ctx.line_width = 1
local center_y = content_y + (content_height / 2)
ctx:begin_path()
ctx:move_to(content_x, center_y)
ctx:line_to(content_x + content_width, center_y)
ctx:stroke()
-- Draw parameter bars - SIMPLE AND CLEAR like PakettiPCMWriter
for i, param_info in ipairs(device_parameters) do
-- CRITICAL: Check if parameter info is valid
if param_info and param_info.parameter then
-- CRITICAL: Check if parameter is still valid (not nil object)
local success, param_value = pcall(function() return param_info.parameter.value end)
if success then
local column_start_x = content_x + (i - 1) * parameter_width
local column_center_x = column_start_x + (parameter_width / 2)
local column_end_x = column_start_x + parameter_width
-- Draw parameter column background - light gray
ctx.stroke_color = {64, 64, 64, 255} -- Dark gray
ctx.line_width = 1
ctx:begin_path()
ctx:move_to(column_start_x, content_y)
ctx:line_to(column_start_x, content_y + content_height)
ctx:stroke()
local value_min = param_info.value_min
local value_max = param_info.value_max