-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathPakettiAutomationStack.lua
More file actions
3294 lines (2971 loc) · 137 KB
/
Copy pathPakettiAutomationStack.lua
File metadata and controls
3294 lines (2971 loc) · 137 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
-- PakettiAutomationStack.lua
-- Visualize all automation envelopes on the selected track within the selected pattern
-- Requirements honored:
-- - Lua 5.1 only
-- - All functions are GLOBAL and defined before first use
-- - Uses my_keyhandler_func from main.lua
-- - After dialog opens, reactivate middle frame to pass keys back to Renoise
-- - Stable ViewBuilder layout (rebuild content instead of toggling visibility)
-- - Uses shared PakettiCanvasFontDrawText when available
-- Global state
PakettiAutomationStack_dialog = nil
PakettiAutomationStack_vb = nil
PakettiAutomationStack_header_canvas = nil
PakettiAutomationStack_container = nil
PakettiAutomationStack_track_canvases = {}
PakettiAutomationStack_canvas_width = 1400
PakettiAutomationStack_lane_height = 80
PakettiAutomationStack_gutter_height = 18
PakettiAutomationStack_gutter_width = 0
PakettiAutomationStack_zoom_levels = {1.0, 0.5, 0.25, 0.125}
PakettiAutomationStack_zoom_index = 1
PakettiAutomationStack_view_start_line = 1
PakettiAutomationStack_scrollbar_view = nil
PakettiAutomationStack_timer_running = false
PakettiAutomationStack_timer_interval_ms = 120
PakettiAutomationStack_last_hash = ""
PakettiAutomationStack_automations = {}
PakettiAutomationStack_vertical_page_index = 1
PakettiAutomationStack_vertical_page_size = 8
PakettiAutomationStack_vscrollbar_view = nil
PakettiAutomationStack_min_redraw_ms = 60
PakettiAutomationStack_min_redraw_ms_idle = 100 -- Slower refresh when not drawing
PakettiAutomationStack_min_redraw_ms_active = 16 -- ~60 FPS when actively drawing
PakettiAutomationStack_last_redraw_ms = 0
PakettiAutomationStack_is_drawing = false
PakettiAutomationStack_last_draw_line = -1
PakettiAutomationStack_last_draw_idx = -1
PakettiAutomationStack_last_draw_value = -1
PakettiAutomationStack_draw_playmode_index = 2 -- 1=Points,2=Lines,3=Curves
PakettiAutomationStack_selection_active = false
PakettiAutomationStack_selection_start_line = nil
PakettiAutomationStack_selection_end_line = nil
PakettiAutomationStack_selection_lane_index = nil
PakettiAutomationStack_show_all_vertically = false
PakettiAutomationStack_count_text_view = nil
PakettiAutomationStack_view_mode = 1 -- 1=Stack (multi lanes), 2=Single (overlay)
PakettiAutomationStack_env_popup_view = nil
PakettiAutomationStack_single_selected_index = 1
PakettiAutomationStack_single_canvas_height = 320
PakettiAutomationStack_copy_buffer = nil
PakettiAutomationStack_automation_hashes = {} -- Individual automation hashes for change detection
-- Single view background caching for performance
PakettiAutomationStack_background_cache = nil -- Cached rendering function for non-selected automations
PakettiAutomationStack_background_cache_valid = false -- Whether cache is valid
PakettiAutomationStack_cached_selected_index = nil -- Track which index was selected when cache was built
PakettiAutomationStack_cached_num_lines = nil -- Track pattern length when cache was built
-- Grid caching for performance
PakettiAutomationStack_grid_cache = nil -- Cached grid rendering function
PakettiAutomationStack_grid_cache_valid = false -- Whether grid cache is valid
PakettiAutomationStack_cached_grid_num_lines = nil -- Track pattern length for grid cache
PakettiAutomationStack_cached_grid_lpb = nil -- Track LPB for grid cache
-- Observable management for auto-updating
PakettiAutomationStack_track_observables = {} -- Track mute state observables
PakettiAutomationStack_tracks_observable = nil -- Track list observable
PakettiAutomationStack_selection_range_observable = nil -- Sequencer selection range observable
-- Arbitrary Parameters Selection System
PakettiAutomationStack_arbitrary_mode = false -- false=current track, true=arbitrary selection
PakettiAutomationStack_selected_parameters = {} -- Array of {track_index, device_index, param_index}
PakettiAutomationStack_arbitrary_dialog = nil
PakettiAutomationStack_arbitrary_vb = nil
PakettiAutomationStack_parameter_checkboxes = {}
-- Show Same Parameters System
PakettiAutomationStack_show_same_mode = false -- false=normal modes, true=show same parameter name
PakettiAutomationStack_selected_param_name = "" -- The parameter name to filter by
PakettiAutomationStack_show_same_popup_view = nil
-- Preferences key for persistent storage
PakettiAutomationStack_prefs_key = "AutomationStack_SelectedParams"
-- Stacker Mode: Multi-pattern automation editing
PakettiAutomationStack_stacker_mode = false -- false=single pattern, true=multi-pattern
PakettiAutomationStack_stacker_seq_start = nil -- Start sequence index
PakettiAutomationStack_stacker_seq_end = nil -- End sequence index
PakettiAutomationStack_stacker_patterns = {} -- Array of {seq_index, pattern_index, pattern, num_lines, start_offset}
PakettiAutomationStack_stacker_total_lines = 0 -- Sum of all pattern lines
-- Utility draw text helper
function PakettiAutomationStack_DrawText(ctx, text, x, y, size)
if type(PakettiCanvasFontDrawText) == "function" then
PakettiCanvasFontDrawText(ctx, text, x, y, size)
else
-- Minimal fallback: small tick + no real text (keeps code safe if font not present)
ctx.stroke_color = {200,200,200,255}
ctx:begin_path(); ctx:move_to(x, y); ctx:line_to(x+6, y); ctx:stroke()
end
end
-- Get track color utility function
function PakettiAutomationStack_GetTrackColor(track_index)
local song = renoise.song()
if not song or track_index < 1 or track_index > #song.tracks then
return {255, 255, 255} -- Default white
end
local track = song.tracks[track_index]
local color = track.color
if not color or #color < 3 then
return {255, 255, 255} -- Default white if no color
end
return {color[1], color[2], color[3]}
end
-- Check if track is muted
function PakettiAutomationStack_IsTrackMuted(track_index)
local song = renoise.song()
if not song or track_index < 1 or track_index > #song.tracks then
return false
end
local track = song.tracks[track_index]
-- Master track doesn't have mute state
if track.type == renoise.Track.TRACK_TYPE_MASTER then
return false
end
return track.mute_state == renoise.Track.MUTE_STATE_MUTED
end
-- Convert track color to canvas colors with alpha and brightness variations
function PakettiAutomationStack_TrackColorToCanvas(track_index, alpha, brightness_mult)
local rgb = PakettiAutomationStack_GetTrackColor(track_index)
alpha = alpha or 255
brightness_mult = brightness_mult or 1.0
-- Apply mute state - if muted, desaturate and darken significantly
local is_muted = PakettiAutomationStack_IsTrackMuted(track_index)
if is_muted then
brightness_mult = brightness_mult * 0.3 -- Much darker when muted
alpha = math.max(60, alpha * 0.4) -- Much more transparent when muted
-- Desaturate by averaging with grey
local grey = (rgb[1] + rgb[2] + rgb[3]) / 3
rgb[1] = rgb[1] * 0.2 + grey * 0.8
rgb[2] = rgb[2] * 0.2 + grey * 0.8
rgb[3] = rgb[3] * 0.2 + grey * 0.8
end
-- Apply brightness multiplier and clamp
local r = math.max(0, math.min(255, math.floor(rgb[1] * brightness_mult)))
local g = math.max(0, math.min(255, math.floor(rgb[2] * brightness_mult)))
local b = math.max(0, math.min(255, math.floor(rgb[3] * brightness_mult)))
return {r, g, b, alpha}
end
-- Get contrasting track colors for automation drawing
function PakettiAutomationStack_GetTrackAutomationColors(track_index)
local base_color = PakettiAutomationStack_TrackColorToCanvas(track_index, 235, 1.0)
local bright_color = PakettiAutomationStack_TrackColorToCanvas(track_index, 255, 1.3)
local dim_color = PakettiAutomationStack_TrackColorToCanvas(track_index, 180, 0.8)
return base_color, bright_color, dim_color
end
-- Generate proper track name based on track type and position
function PakettiAutomationStack_GetTrackName(track_index)
local song = renoise.song()
if not song or track_index < 1 or track_index > #song.tracks then return "Unknown" end
local track = song.tracks[track_index]
if track.type == renoise.Track.TRACK_TYPE_MASTER then
return track.name or "Master"
elseif track.type == renoise.Track.TRACK_TYPE_SEND then
return track.name or ("Send " .. string.format("%02d", track_index))
elseif track.type == renoise.Track.TRACK_TYPE_GROUP then
return track.name or ("Group " .. string.format("%02d", track_index))
else -- TRACK_TYPE_SEQUENCER (regular tracks)
-- Count sequencer tracks up to this point for proper numbering
local sequencer_count = 0
for i = 1, track_index do
if song.tracks[i].type == renoise.Track.TRACK_TYPE_SEQUENCER then
sequencer_count = sequencer_count + 1
end
end
return track.name or ("Track " .. string.format("%02d", sequencer_count))
end
end
-- Toggle track mute state
function PakettiAutomationStack_ToggleTrackMute(track_index)
local song = renoise.song()
if not song or track_index < 1 or track_index > #song.tracks then return end
local track = song.tracks[track_index]
-- Master track doesn't have mute state
if track.type == renoise.Track.TRACK_TYPE_MASTER then return end
if track.mute_state == renoise.Track.MUTE_STATE_MUTED then
track:unmute()
renoise.app():show_status("Automation Stack: Unmuted " .. PakettiAutomationStack_GetTrackName(track_index))
else
track:mute()
renoise.app():show_status("Automation Stack: Muted " .. PakettiAutomationStack_GetTrackName(track_index))
end
end
-- Key handler wrapper to support flood fill with Enter while delegating to my_keyhandler_func
function PakettiAutomationStack_KeyHandler(dialog, key)
if key and key.modifiers == "" and (key.name == "return" or key.name == "enter") then
local song, patt, _ptrack = PakettiAutomationStack_GetSongPatternTrack()
if song and patt and PakettiAutomationStack_selection_active and PakettiAutomationStack_selection_lane_index then
local start_l = PakettiAutomationStack_selection_start_line or 1
local end_l = PakettiAutomationStack_selection_end_line or patt.number_of_lines
if start_l > end_l then local tmp = start_l; start_l = end_l; end_l = tmp end
local v = PakettiAutomationStack_last_draw_value
if not v then v = 0.0 end
for L = start_l, end_l do
PakettiAutomationStack_WritePoint(PakettiAutomationStack_selection_lane_index, L, v, false)
end
PakettiAutomationStack_selection_active = false
PakettiAutomationStack_selection_lane_index = nil
PakettiAutomationStack_selection_start_line = nil
PakettiAutomationStack_selection_end_line = nil
PakettiAutomationStack_RequestUpdate()
renoise.app():show_status("Automation Stack: Flood filled lines " .. tostring(start_l) .. "-" .. tostring(end_l))
return nil
end
end
if type(my_keyhandler_func) == "function" then
return my_keyhandler_func(dialog, key)
end
return key
end
-- Safe access helpers
function PakettiAutomationStack_GetSongPatternTrack()
local song = renoise.song()
if not song then return nil, nil, nil end
local patt_idx = song.selected_pattern_index
if patt_idx < 1 or patt_idx > #song.patterns then return song, nil, nil end
local patt = song:pattern(patt_idx)
local track_idx = song.selected_track_index
if track_idx < 1 or track_idx > #song.tracks then return song, patt, nil end
local ptrack = patt:track(track_idx)
return song, patt, ptrack
end
-- Build stacker patterns array from sequencer selection
function PakettiAutomationStack_BuildStackerPatterns()
PakettiAutomationStack_stacker_patterns = {}
PakettiAutomationStack_stacker_total_lines = 0
PakettiAutomationStack_stacker_seq_start = nil
PakettiAutomationStack_stacker_seq_end = nil
local song = renoise.song()
if not song then return false end
local selection_range = song.sequencer.selection_range
-- selection_range returns {0, 0} if no selection, else {start, end}
if not selection_range or selection_range[1] == 0 or selection_range[2] == 0 then
-- No selection - disable stacker mode
return false
end
local seq_start = selection_range[1]
local seq_end = selection_range[2]
-- Ensure valid range
if seq_start > seq_end then
local tmp = seq_start
seq_start = seq_end
seq_end = tmp
end
-- Clamp to valid sequence range
local seq_count = #song.sequencer.pattern_sequence
if seq_start < 1 then seq_start = 1 end
if seq_end > seq_count then seq_end = seq_count end
PakettiAutomationStack_stacker_seq_start = seq_start
PakettiAutomationStack_stacker_seq_end = seq_end
local cumulative_offset = 0
for seq_idx = seq_start, seq_end do
local pattern_index = song.sequencer.pattern_sequence[seq_idx]
local pattern = song:pattern(pattern_index)
local num_lines = pattern.number_of_lines
local entry = {
seq_index = seq_idx,
pattern_index = pattern_index,
pattern = pattern,
num_lines = num_lines,
start_offset = cumulative_offset -- 0-based offset for this pattern's first line
}
PakettiAutomationStack_stacker_patterns[#PakettiAutomationStack_stacker_patterns + 1] = entry
cumulative_offset = cumulative_offset + num_lines
end
PakettiAutomationStack_stacker_total_lines = cumulative_offset
print("Stacker: Built " .. #PakettiAutomationStack_stacker_patterns .. " patterns, total " .. PakettiAutomationStack_stacker_total_lines .. " lines")
return true
end
-- Convert global line (1-based across all patterns) to pattern-local coordinates
-- Returns: seq_index, pattern_index, local_line, pattern_entry_index
function PakettiAutomationStack_GlobalLineToPatternLine(global_line)
if not PakettiAutomationStack_stacker_mode or #PakettiAutomationStack_stacker_patterns == 0 then
-- Not in stacker mode, return current pattern info
local song = renoise.song()
if not song then return nil, nil, global_line, nil end
local seq_idx = song.selected_sequence_index
local patt_idx = song.selected_pattern_index
return seq_idx, patt_idx, global_line, nil
end
-- Find which pattern this global line falls into
local global_0based = global_line - 1
for i, entry in ipairs(PakettiAutomationStack_stacker_patterns) do
local pattern_end_offset = entry.start_offset + entry.num_lines
if global_0based < pattern_end_offset then
local local_line = global_0based - entry.start_offset + 1
return entry.seq_index, entry.pattern_index, local_line, i
end
end
-- Past the end - return last pattern's last line
local last = PakettiAutomationStack_stacker_patterns[#PakettiAutomationStack_stacker_patterns]
if last then
return last.seq_index, last.pattern_index, last.num_lines, #PakettiAutomationStack_stacker_patterns
end
return nil, nil, global_line, nil
end
-- Convert pattern-local line to global line (1-based)
function PakettiAutomationStack_PatternLineToGlobalLine(pattern_entry_index, local_line)
if not PakettiAutomationStack_stacker_mode or #PakettiAutomationStack_stacker_patterns == 0 then
return local_line
end
local entry = PakettiAutomationStack_stacker_patterns[pattern_entry_index]
if not entry then return local_line end
return entry.start_offset + local_line
end
-- Get total lines for current view (stacker or single pattern)
function PakettiAutomationStack_GetTotalLines()
if PakettiAutomationStack_stacker_mode and PakettiAutomationStack_stacker_total_lines > 0 then
return PakettiAutomationStack_stacker_total_lines
end
local song, patt, _ = PakettiAutomationStack_GetSongPatternTrack()
if patt then
return patt.number_of_lines
end
return 64 -- Default fallback
end
-- Zoom and mapping
function PakettiAutomationStack_GetWindowLines(total_lines)
local z = PakettiAutomationStack_zoom_levels[PakettiAutomationStack_zoom_index] or 1.0
local win = math.max(1, math.floor(total_lines * z + 0.5))
if win > total_lines then win = total_lines end
return win
end
function PakettiAutomationStack_ClampView(total_lines)
local win = PakettiAutomationStack_GetWindowLines(total_lines)
local max_start = math.max(1, total_lines - win + 1)
if PakettiAutomationStack_view_start_line < 1 then PakettiAutomationStack_view_start_line = 1 end
if PakettiAutomationStack_view_start_line > max_start then PakettiAutomationStack_view_start_line = max_start end
end
function PakettiAutomationStack_MapTimeToX(t, total_lines)
local win = PakettiAutomationStack_GetWindowLines(total_lines)
local view_start_t = (PakettiAutomationStack_view_start_line - 1)
-- Account for gutters - use effective width, not full canvas width
local eff_w = math.max(1, PakettiAutomationStack_canvas_width - (2*PakettiAutomationStack_gutter_width))
if win <= 1 then
return PakettiAutomationStack_gutter_width + eff_w / 2
end
-- Map the pattern length to canvas width, but extend slightly beyond the last line
-- This allows clicking at the right edge to map to the last valid line
local divisor = total_lines + 0.9 -- Add 0.9 to allow clicking at right edge to reach line 64
local x = PakettiAutomationStack_gutter_width + ((t - view_start_t) / divisor) * eff_w
return x
end
function PakettiAutomationStack_LineToX(line_index, total_lines)
return PakettiAutomationStack_MapTimeToX((line_index - 1), total_lines)
end
-- Inverse mapping: canvas x to pattern line
function PakettiAutomationStack_XToLine(x, total_lines)
local win = PakettiAutomationStack_GetWindowLines(total_lines)
local view_start_t = (PakettiAutomationStack_view_start_line - 1)
local eff_w = math.max(1, PakettiAutomationStack_canvas_width - (2*PakettiAutomationStack_gutter_width))
-- Remove gutter offset first (inverse of forward mapping)
local x_eff = x - PakettiAutomationStack_gutter_width
-- Map effective canvas width to 0-based time (inverse of forward mapping)
-- Use the same extended divisor as the forward mapping
local divisor = total_lines + 0.9
local t_0based = (x_eff / eff_w) * divisor
-- Add back the view start offset to get the actual time
local actual_time = t_0based + view_start_t
-- Convert from 0-based time back to 1-based line number
-- Use proper rounding to avoid off-by-one errors
-- Allow clicking at right edge to reach line 65 (the actual end)
local line = math.max(1, math.min(total_lines + 1, math.floor(actual_time + 0.5) + 1))
return line
end
function PakettiAutomationStack_UpdateScrollbars()
local song, patt, _ptrack = PakettiAutomationStack_GetSongPatternTrack()
if not song or not patt then return end
-- Use stacker total lines if in stacker mode
local num_lines = PakettiAutomationStack_GetTotalLines()
PakettiAutomationStack_ClampView(num_lines)
if PakettiAutomationStack_scrollbar_view then
local win = PakettiAutomationStack_GetWindowLines(num_lines)
PakettiAutomationStack_scrollbar_view.min = 1
PakettiAutomationStack_scrollbar_view.max = num_lines + 1
PakettiAutomationStack_scrollbar_view.pagestep = win
PakettiAutomationStack_scrollbar_view.value = PakettiAutomationStack_view_start_line
end
end
function PakettiAutomationStack_SetZoomIndex(idx)
PakettiAutomationStack_zoom_index = math.max(1, math.min(#PakettiAutomationStack_zoom_levels, idx))
PakettiAutomationStack_UpdateScrollbars()
PakettiAutomationStack_RequestUpdate()
end
-- Hash builder for quick change detection
function PakettiAutomationStack_BuildAutomationHash()
local song, patt, ptrack = PakettiAutomationStack_GetSongPatternTrack()
if not song or not patt or not ptrack then return "" end
local acc = {tostring(patt.number_of_lines), tostring(song.selected_track_index), tostring(#song.tracks)}
-- Include track count and mute states for change detection
for track_idx = 1, #song.tracks do
local track = song.tracks[track_idx]
if track then
local mute_state = "active"
if track.type ~= renoise.Track.TRACK_TYPE_MASTER then
if track.mute_state == renoise.Track.MUTE_STATE_MUTED then
mute_state = "muted"
elseif track.mute_state == renoise.Track.MUTE_STATE_OFF then
mute_state = "off"
end
end
acc[#acc+1] = "t" .. tostring(track_idx) .. ":" .. mute_state .. ":" .. (track.name or "")
end
end
local track = song.tracks[song.selected_track_index]
if not track then return table.concat(acc, ":") end
for d = 1, #track.devices do
local dev = track.devices[d]
for pi = 1, #dev.parameters do
local param = dev.parameters[pi]
if param.is_automatable then
local a = ptrack:find_automation(param)
if a then
local ok1, pm = pcall(function() return a.playmode end)
acc[#acc+1] = tostring(pm or "?")
acc[#acc+1] = tostring(param.name or "?")
local points = a.points
if points then
acc[#acc+1] = tostring(#points)
local sumt = 0; local sumv = 0
for j = 1, #points do
local p = points[j]
sumt = sumt + (p.time or 0)
sumv = sumv + math.floor(((p.value or 0)*1000)+0.5)
end
acc[#acc+1] = tostring(sumt)
acc[#acc+1] = tostring(sumv)
end
end
end
end
end
return table.concat(acc, ":")
end
-- Build individual automation hashes for change detection
function PakettiAutomationStack_BuildIndividualHashes()
local song, patt, ptrack = PakettiAutomationStack_GetSongPatternTrack()
if not song or not patt or not ptrack then return {} end
local hashes = {}
local track = song.tracks[song.selected_track_index]
if not track then return hashes end
local auto_index = 1
for d = 1, #track.devices do
local dev = track.devices[d]
for pi = 1, #dev.parameters do
local param = dev.parameters[pi]
if param.is_automatable then
local a = ptrack:find_automation(param)
if a then
local acc = {}
local ok1, pm = pcall(function() return a.playmode end)
acc[#acc+1] = tostring(pm or "?")
acc[#acc+1] = tostring(param.name or "?")
local points = a.points
if points then
acc[#acc+1] = tostring(#points)
local sumt = 0; local sumv = 0
for j = 1, #points do
local p = points[j]
sumt = sumt + (p.time or 0)
sumv = sumv + math.floor(((p.value or 0)*1000)+0.5)
end
acc[#acc+1] = tostring(sumt)
acc[#acc+1] = tostring(sumv)
end
hashes[auto_index] = table.concat(acc, ":")
auto_index = auto_index + 1
end
end
end
end
return hashes
end
-- Detect which automation changed and auto-select it
function PakettiAutomationStack_DetectChangedAutomation()
local new_hashes = PakettiAutomationStack_BuildIndividualHashes()
for i = 1, #new_hashes do
local old_hash = PakettiAutomationStack_automation_hashes[i]
local new_hash = new_hashes[i]
if old_hash and old_hash ~= new_hash then
-- This automation changed, auto-select it
if PakettiAutomationStack_view_mode == 2 then
-- Single view mode
PakettiAutomationStack_single_selected_index = i
PakettiAutomationStack_UpdateEnvPopup()
local entry = PakettiAutomationStack_automations[i]
if entry then
renoise.app():show_status("Auto-selected: " .. (entry.device_name or "Device") .. ": " .. (entry.name or "Parameter"))
end
end
break -- Only select the first changed automation
end
end
PakettiAutomationStack_automation_hashes = new_hashes
end
-- Enumerate all automatable parameters from all tracks
function PakettiAutomationStack_GetAllParameters()
local song = renoise.song()
if not song then return {} end
local all_params = {}
for track_idx = 1, #song.tracks do
local track = song.tracks[track_idx]
local track_name = PakettiAutomationStack_GetTrackName(track_idx)
for dev_idx = 1, #track.devices do
local dev = track.devices[dev_idx]
local device_name = dev.display_name or "Device"
for param_idx = 1, #dev.parameters do
local param = dev.parameters[param_idx]
if param.is_automatable then
local param_key = track_idx .. "_" .. dev_idx .. "_" .. param_idx
local display_name = string.format("Track%02d: %s: %s",
track_idx, device_name, param.name or "Parameter")
all_params[#all_params+1] = {
key = param_key,
track_index = track_idx,
device_index = dev_idx,
param_index = param_idx,
track_name = track_name,
device_name = device_name,
param_name = param.name or "Parameter",
display_name = display_name,
parameter = param
}
end
end
end
end
return all_params
end
-- Get all unique parameter names that currently have automation data
function PakettiAutomationStack_GetUniqueAutomatedParameterNames()
local song, patt = PakettiAutomationStack_GetSongPatternTrack()
if not song or not patt then return {} end
local unique_names = {}
local name_counts = {} -- Track how many times each name appears
for track_idx = 1, #song.tracks do
local track = song.tracks[track_idx]
local pattern_track = patt:track(track_idx)
if pattern_track then
for dev_idx = 1, #track.devices do
local dev = track.devices[dev_idx]
for param_idx = 1, #dev.parameters do
local param = dev.parameters[param_idx]
if param.is_automatable then
local automation = pattern_track:find_automation(param)
if automation and automation.points and #automation.points > 0 then
local param_name = param.name or "Parameter"
if not name_counts[param_name] then
name_counts[param_name] = 0
unique_names[#unique_names+1] = param_name
end
name_counts[param_name] = name_counts[param_name] + 1
end
end
end
end
end
end
-- Sort by name and add count information
table.sort(unique_names)
local result = {}
for i = 1, #unique_names do
local name = unique_names[i]
local count = name_counts[name]
local display_name = string.format("%s (%d)", name, count)
result[#result+1] = {
name = name,
count = count,
display_name = display_name
}
end
return result
end
-- Check if a parameter is selected for arbitrary display
function PakettiAutomationStack_IsParameterSelected(track_idx, dev_idx, param_idx)
for i = 1, #PakettiAutomationStack_selected_parameters do
local sel = PakettiAutomationStack_selected_parameters[i]
if sel.track_index == track_idx and sel.device_index == dev_idx and sel.param_index == param_idx then
return true
end
end
return false
end
-- Add parameter to selection
function PakettiAutomationStack_AddParameter(track_idx, dev_idx, param_idx)
if not PakettiAutomationStack_IsParameterSelected(track_idx, dev_idx, param_idx) then
PakettiAutomationStack_selected_parameters[#PakettiAutomationStack_selected_parameters+1] = {
track_index = track_idx,
device_index = dev_idx,
param_index = param_idx
}
end
end
-- Remove parameter from selection
function PakettiAutomationStack_RemoveParameter(track_idx, dev_idx, param_idx)
for i = #PakettiAutomationStack_selected_parameters, 1, -1 do
local sel = PakettiAutomationStack_selected_parameters[i]
if sel.track_index == track_idx and sel.device_index == dev_idx and sel.param_index == param_idx then
table.remove(PakettiAutomationStack_selected_parameters, i)
break
end
end
end
-- Save selected parameters to preferences
function PakettiAutomationStack_SaveSelectedParameters()
local params_table = {}
for i = 1, #PakettiAutomationStack_selected_parameters do
local sel = PakettiAutomationStack_selected_parameters[i]
local param_string = sel.track_index .. "_" .. sel.device_index .. "_" .. sel.param_index
params_table[#params_table+1] = param_string
end
renoise.tool().preferences[PakettiAutomationStack_prefs_key] = table.concat(params_table, ",")
end
-- Load selected parameters from preferences
function PakettiAutomationStack_LoadSelectedParameters()
local params_string = renoise.tool().preferences[PakettiAutomationStack_prefs_key]
PakettiAutomationStack_selected_parameters = {}
if params_string and params_string.value and params_string.value ~= "" then
local param_strings = {}
for param_str in params_string.value:gmatch("[^,]+") do
param_strings[#param_strings+1] = param_str
end
for i = 1, #param_strings do
local parts = {}
for part in param_strings[i]:gmatch("[^_]+") do
parts[#parts+1] = tonumber(part)
end
if #parts == 3 then
PakettiAutomationStack_selected_parameters[#PakettiAutomationStack_selected_parameters+1] = {
track_index = parts[1],
device_index = parts[2],
param_index = parts[3]
}
end
end
end
end
-- Rebuild automation list (modified to support arbitrary parameters and show same)
function PakettiAutomationStack_RebuildAutomations()
PakettiAutomationStack_automations = {}
local song, patt, ptrack = PakettiAutomationStack_GetSongPatternTrack()
if not song or not patt then return end
if PakettiAutomationStack_show_same_mode then
-- Show Same mode: find all automations with the selected parameter name
if PakettiAutomationStack_selected_param_name and PakettiAutomationStack_selected_param_name ~= "" then
for track_idx = 1, #song.tracks do
local track = song.tracks[track_idx]
local pattern_track = patt:track(track_idx)
if pattern_track then
for dev_idx = 1, #track.devices do
local dev = track.devices[dev_idx]
for param_idx = 1, #dev.parameters do
local param = dev.parameters[param_idx]
if param.is_automatable and (param.name or "Parameter") == PakettiAutomationStack_selected_param_name then
local a = pattern_track:find_automation(param)
-- Show parameter even if no automation exists yet (allows creating new automation)
local entry = {
automation = a, -- May be nil for new automation
parameter = param,
pattern_track = pattern_track, -- Store for creating automation later
name = param.name or "Parameter",
device_name = dev.display_name or "Device",
track_name = PakettiAutomationStack_GetTrackName(track_idx),
track_index = track_idx,
playmode = a and a.playmode or PAKETTI_PLAYMODE_LINES
}
PakettiAutomationStack_automations[#PakettiAutomationStack_automations+1] = entry
end
end
end
end
end
end
elseif PakettiAutomationStack_arbitrary_mode then
-- Arbitrary mode: show selected parameters from any track
for i = 1, #PakettiAutomationStack_selected_parameters do
local sel = PakettiAutomationStack_selected_parameters[i]
local track = song.tracks[sel.track_index]
if track then
local pattern_track = patt:track(sel.track_index)
if pattern_track then
local dev = track.devices[sel.device_index]
if dev then
local param = dev.parameters[sel.param_index]
if param and param.is_automatable then
local a = pattern_track:find_automation(param)
-- Show parameter even if no automation exists yet (allows creating new automation)
local entry = {
automation = a, -- May be nil for new automation
parameter = param,
pattern_track = pattern_track, -- Store for creating automation later
name = param.name or "Parameter",
device_name = dev.display_name or "Device",
track_name = PakettiAutomationStack_GetTrackName(sel.track_index),
track_index = sel.track_index,
playmode = a and a.playmode or PAKETTI_PLAYMODE_LINES
}
PakettiAutomationStack_automations[#PakettiAutomationStack_automations+1] = entry
end
end
end
end
end
else
-- Current track mode: prioritize parameters with automation data
if not ptrack then return end
local track = song.tracks[song.selected_track_index]
if not track then return end
local automations_with_data = {}
local automations_without_data = {}
-- First pass: collect all automatable parameters
for d = 1, #track.devices do
local dev = track.devices[d]
for pi = 1, #dev.parameters do
local param = dev.parameters[pi]
if param.is_automatable then
local a = ptrack:find_automation(param)
local entry = {
automation = a, -- May be nil for new automation
parameter = param,
pattern_track = ptrack, -- Store for creating automation later
name = param.name or "Parameter",
device_name = dev.display_name or "Device",
track_name = PakettiAutomationStack_GetTrackName(song.selected_track_index),
track_index = song.selected_track_index,
playmode = a and a.playmode or PAKETTI_PLAYMODE_LINES,
device_index = d,
param_index = pi
}
-- Check if this parameter has automation data
local has_data = false
if a and a.points and #a.points > 0 then
has_data = true
end
if has_data then
automations_with_data[#automations_with_data+1] = entry
else
-- Skip mixer parameters unless they have data
local is_mixer_param = (param.name == "Panning" or param.name == "Volume" or param.name == "Width")
if not is_mixer_param then
automations_without_data[#automations_without_data+1] = entry
end
end
end
end
end
-- Add automations with data first
for i = 1, #automations_with_data do
PakettiAutomationStack_automations[#PakettiAutomationStack_automations+1] = automations_with_data[i]
end
-- If no automations with data, show first device's first few parameters
if #automations_with_data == 0 then
local first_device_params = {}
for i = 1, #automations_without_data do
local entry = automations_without_data[i]
if entry.device_index == 1 then -- First device only
first_device_params[#first_device_params+1] = entry
end
end
-- Add first 4 parameters from first device
for i = 1, math.min(4, #first_device_params) do
PakettiAutomationStack_automations[#PakettiAutomationStack_automations+1] = first_device_params[i]
end
end
end
PakettiAutomationStack_UpdateEnvPopup()
-- Invalidate background cache since automation list changed
PakettiAutomationStack_InvalidateBackgroundCache()
end
-- Redraw throttling with context-aware rates
function PakettiAutomationStack_RequestUpdate()
local now_ms = os.clock() * 1000
-- Use faster throttle when actively drawing, slower when idle
local throttle_ms = PakettiAutomationStack_is_drawing and PakettiAutomationStack_min_redraw_ms_active or PakettiAutomationStack_min_redraw_ms_idle
if (now_ms - PakettiAutomationStack_last_redraw_ms) >= throttle_ms then
if PakettiAutomationStack_header_canvas and PakettiAutomationStack_header_canvas.visible then PakettiAutomationStack_header_canvas:update() end
if PakettiAutomationStack_track_canvases and #PakettiAutomationStack_track_canvases > 0 then
for i = 1, #PakettiAutomationStack_track_canvases do
local c = PakettiAutomationStack_track_canvases[i]
if c and c.visible then c:update() end
end
end
PakettiAutomationStack_last_redraw_ms = now_ms
end
end
-- Rendering helpers
function PakettiAutomationStack_DrawGrid(ctx, W, H, num_lines, lpb)
local win = PakettiAutomationStack_GetWindowLines(num_lines)
local pixels_per_line = W / win
-- background
ctx:set_fill_linear_gradient(0, 0, 0, H)
ctx:add_fill_color_stop(0, {25,25,35,255})
ctx:add_fill_color_stop(1, {15,15,25,255})
ctx:begin_path(); ctx:rect(0,0,W,H); ctx:fill()
-- gutters (only if gutter_width > 0)
local gutter = PakettiAutomationStack_gutter_width
if gutter > 0 then
ctx.fill_color = {18,18,24,255}; ctx:fill_rect(0, 0, gutter, H)
ctx.fill_color = {14,14,20,255}; ctx:fill_rect(W - gutter, 0, gutter, H)
end
-- grid
if pixels_per_line >= 6 then
for line = PakettiAutomationStack_view_start_line, math.min(num_lines, PakettiAutomationStack_view_start_line + win - 1) do
local x = PakettiAutomationStack_LineToX(line, num_lines)
if ((line-1) % (lpb*4)) == 0 then
ctx.stroke_color = {110,110,160,255}; ctx.line_width = 3
elseif ((line-1) % lpb) == 0 then
ctx.stroke_color = {80,80,120,220}; ctx.line_width = 2
else
ctx.stroke_color = {40,40,60,130}; ctx.line_width = 1
end
ctx:begin_path(); ctx:move_to(x, 0); ctx:line_to(x, H); ctx:stroke()
end
else
ctx.line_width = 1
for line = PakettiAutomationStack_view_start_line, math.min(num_lines, PakettiAutomationStack_view_start_line + win - 1) do
if ((line-1) % lpb) == 0 then
local x = PakettiAutomationStack_LineToX(line, num_lines)
ctx.stroke_color = {70,70,100,220}
ctx:begin_path(); ctx:move_to(x, 0); ctx:line_to(x, H); ctx:stroke()
end
end
end
-- Draw pattern boundaries in stacker mode
if PakettiAutomationStack_stacker_mode and #PakettiAutomationStack_stacker_patterns > 1 then
ctx.line_width = 2
for i = 2, #PakettiAutomationStack_stacker_patterns do
local entry = PakettiAutomationStack_stacker_patterns[i]
-- Pattern boundary is at start_offset + 1 (first line of this pattern in global coords)
local boundary_line = entry.start_offset + 1
local x = PakettiAutomationStack_LineToX(boundary_line, num_lines)
-- Draw dashed orange line for pattern boundary
ctx.stroke_color = {255, 140, 0, 255}
local dash_len = 6
local gap_len = 4
local y = 0
while y < H do
ctx:begin_path()
ctx:move_to(x, y)
ctx:line_to(x, math.min(y + dash_len, H))
ctx:stroke()
y = y + dash_len + gap_len
end
end
end
end
-- Draw a single automation into an existing canvas with track-based colors or custom colors
function PakettiAutomationStack_DrawAutomation(entry, ctx, W, H, num_lines, color_main, color_point, line_width, use_track_colors)
if not entry or not entry.automation then return end
local a = entry.automation
local points = a.points or {}
local mode = a.playmode or PAKETTI_PLAYMODE_POINTS
local gutter = PakettiAutomationStack_gutter_width
if #points == 0 then return end
-- Use track colors if enabled and we have a track index, otherwise use provided colors
local main_color = color_main
local point_color = color_point
if use_track_colors and entry.track_index then
local base_color, bright_color, dim_color = PakettiAutomationStack_GetTrackAutomationColors(entry.track_index)
main_color = base_color
point_color = bright_color
end
if mode == PAKETTI_PLAYMODE_POINTS then
ctx.stroke_color = main_color
ctx.line_width = line_width
for i = 1, #points do
local p = points[i]
local x = PakettiAutomationStack_MapTimeToX((p.time or 1) - 1, num_lines)
local y = PakettiAutomationStack_ValueToY(p.value, H)
-- Remove gutter constraints since gutter_width = 0
if gutter > 0 then
if x < gutter then x = gutter end
if x > W - gutter then x = W - gutter end
end
ctx:begin_path(); ctx:move_to(x, H-1); ctx:line_to(x, y); ctx:stroke()
ctx.stroke_color = point_color
ctx.line_width = math.max(1, line_width - 1)
ctx:begin_path(); ctx:move_to(x-1, y); ctx:line_to(x+1, y); ctx:stroke()
ctx.stroke_color = main_color
ctx.line_width = line_width
end
elseif mode == PAKETTI_PLAYMODE_LINES then
ctx.stroke_color = main_color
ctx.line_width = line_width
ctx:begin_path()
local p1 = points[1]
local x1 = PakettiAutomationStack_MapTimeToX((p1.time or 1) - 1, num_lines)
local y1 = PakettiAutomationStack_ValueToY(p1.value, H)
if x1 < gutter then x1 = gutter end
if x1 > W - gutter then x1 = W - gutter end
ctx:move_to(x1, y1)
for i = 2, #points do
local p = points[i]
local x = PakettiAutomationStack_MapTimeToX((p.time or 1) - 1, num_lines)
local y = PakettiAutomationStack_ValueToY(p.value, H)
-- Remove gutter constraints since gutter_width = 0
if gutter > 0 then
if x < gutter then x = gutter end
if x > W - gutter then x = W - gutter end