forked from esaruoho/paketti
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPakettiFill.lua
More file actions
1709 lines (1504 loc) · 65.4 KB
/
PakettiFill.lua
File metadata and controls
1709 lines (1504 loc) · 65.4 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
-- PakettiFill.lua
-- Advanced pattern filling tool with density control, note ranges, and random effects
local dialog = nil
local vb = nil
-- Note names for conversion
local note_names = {
"C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"
}
-- Effects list from PakettiPatternEditorCheatSheet.lua with blacklist capability
local effects_list = {
{"0A", "-Axy", "Set arpeggio, x/y = first/second note offset in semitones", false},
{"0U", "-Uxx", "Slide Pitch up by xx 1/16ths of a semitone", false},
{"0D", "-Dxx", "Slide Pitch down by xx 1/16ths of a semitone", false},
{"0G", "-Gxx", "Glide towards given note by xx 1/16ths of a semitone", false},
{"0I", "-Ixx", "Fade Volume in by xx volume units", false},
{"0O", "-Oxx", "Fade Volume out by xx volume units", false},
{"0C", "-Cxy", "Cut volume to x after y ticks (x = volume factor: 0=0%, F=100%)", false},
{"0Q", "-Qxx", "Delay note by xx ticks", false},
{"0M", "-Mxx", "Set note volume to xx", false},
{"0S", "-Sxx", "Trigger sample slice number xx or offset xx", false},
{"0B", "-Bxx", "Play Sample Backwards (B00) or forwards again (B01)", false},
{"0R", "-Rxy", "Retrigger line every y ticks with volume factor x", false},
{"0Y", "-Yxx", "Maybe trigger line with probability xx, 00 = mutually exclusive note columns", false},
{"0Z", "-Zxx", "Trigger Phrase xx (Phrase Number (01-7E), 00 = none, 7F = keymap)", false},
{"0V", "-Vxy", "Set Vibrato x = speed, y = depth; x=(0-F); y=(0-F)", false},
{"0T", "-Txy", "Set Tremolo x = speed, y = depth", false},
{"0N", "-Nxy", "Set Auto Pan, x = speed, y = depth", false},
{"0E", "-Exx", "Set Active Sample Envelope's Position to Offset XX", false},
{"0L", "-Lxx", "Set Track Volume Level, 00 = -INF, FF = +3dB", true},
{"0P", "-Pxx", "Set Track Pan, 00 = full left, 80 = center, FF = full right", true},
{"0W", "-Wxx", "Set Track Surround Width, 00 = Min, FF = Max", true},
{"0J", "-Jxx", "Set Track Routing, 01 upwards = hardware channels, FF downwards = parent groups", true},
{"0X", "-Xxx", "Stop all notes and FX (xx = 00), or only effect xx (xx > 00)", true},
{"ZT", "ZTxx", "Set tempo to xx BPM (14-FF, 00 = stop song)", true}, -- blacklisted by default
{"ZL", "ZLxx", "Set Lines Per Beat (LPB) to xx lines", true}, -- blacklisted by default
{"ZK", "ZKxx", "Set Ticks Per Line (TPL) to xx ticks (01-10)", true}, -- blacklisted by default
{"ZG", "ZGxx", "Enable (xx = 01) or disable (xx = 00) Groove", true},
{"ZB", "ZBxx", "Break pattern and jump to line xx in next", true}, -- blacklisted by default
{"ZD", "ZDxx", "Delay (pause) pattern for xx lines", true} -- blacklisted by default
}
-- Convert note name and octave to note value
function PakettiFillNoteToValue(note_name, octave)
for i, name in ipairs(note_names) do
if name == note_name then
return (octave * 12) + (i - 1)
end
end
return 48 -- Default to C-4 if not found
end
-- Convert note value to note name and octave
function PakettiFillValueToNote(note_value)
local octave = math.floor(note_value / 12)
local note_index = (note_value % 12) + 1
return note_names[note_index], octave
end
-- Generate random note within range (simple version)
function PakettiFillGenerateRandomNote(from_note, to_note)
if from_note > to_note then
from_note, to_note = to_note, from_note
end
-- Clamp to valid note range
from_note = math.max(0, math.min(119, from_note))
to_note = math.max(0, math.min(119, to_note))
return math.random(from_note, to_note)
end
-- Get all actually mapped notes for current instrument
function PakettiFillGetMappedNotes()
local song = renoise.song()
local instrument = song:instrument(song.selected_instrument_index)
if not instrument or #instrument.samples == 0 then
return {48} -- Default C-4
end
-- Collect all actually mapped notes
local mapped_notes = {}
local first_sample_has_slices = false
local slice_count = 0
local slice_start_note = nil
-- Check if first sample has slice markers
if #instrument.samples > 0 then
local first_sample = instrument:sample(1)
if first_sample and #first_sample.slice_markers > 0 then
first_sample_has_slices = true
slice_count = #first_sample.slice_markers + 1
-- Get the slice start note
local first_mapping = first_sample.sample_mapping
if first_mapping and first_mapping.note_range and #first_mapping.note_range >= 2 then
slice_start_note = first_mapping.note_range[1]
end
end
end
local start_sample_index = 1
-- If first sample has slices, include slice notes and start from second sample for regular samples
if first_sample_has_slices then
-- Add slice notes to mapped notes
if slice_start_note then
for i = 0, slice_count - 1 do
local note = slice_start_note + i
if note <= 119 then -- Clamp to max valid note
table.insert(mapped_notes, note)
end
end
end
-- Start regular sample scanning from second sample (if it exists)
if #instrument.samples > 1 then
start_sample_index = 2
else
start_sample_index = #instrument.samples + 1 -- Skip regular sample loop
end
end
-- Scan through regular samples and their mappings
for sample_index = start_sample_index, #instrument.samples do
local sample = instrument:sample(sample_index)
-- Get the sample mapping for this sample
local mapping = sample.sample_mapping
if mapping and mapping.note_range and #mapping.note_range >= 2 then
-- Add all notes in this mapping's range
for note = mapping.note_range[1], mapping.note_range[2] do
table.insert(mapped_notes, note)
end
end
end
-- Return sorted mapped notes, or default
if #mapped_notes > 0 then
table.sort(mapped_notes)
return mapped_notes
else
return {48} -- Default C-4
end
end
-- Generate random note from actually mapped samples (for Euclidean mode)
function PakettiFillGenerateRandomMappedNote()
local mapped_notes = PakettiFillGetMappedNotes()
return mapped_notes[math.random(1, #mapped_notes)]
end
-- Generate distributed note from mapped samples (for Euclidean From-To mode)
function PakettiFillGenerateDistributedMappedNote(ratio)
local mapped_notes = PakettiFillGetMappedNotes()
local index = math.max(1, math.min(#mapped_notes, math.floor(ratio * #mapped_notes) + 1))
return mapped_notes[index]
end
-- Get available effects (not blacklisted)
function PakettiFillGetAvailableEffects()
local available = {}
for _, effect in ipairs(effects_list) do
if not effect[4] then -- not blacklisted
table.insert(available, effect)
end
end
return available
end
-- Generate appropriate random value for specific effects
function PakettiFillGenerateEffectValue(effect_command)
if effect_command == "0B" then
-- Backwards/Forwards: only 00 or 01
return math.random(0, 1)
elseif effect_command == "0C" then
-- Cut volume: xy format where x=volume factor (0-F), y=ticks (0-F)
local x = math.random(0, 15)
local y = math.random(0, 15)
return (x * 16) + y
elseif effect_command == "0Z" then
-- Phrase trigger: 00=none, 01-7E=phrase numbers, 7F=keymap
return math.random(0, 127)
elseif effect_command == "0V" or effect_command == "0T" or effect_command == "0N" then
-- Vibrato/Tremolo/AutoPan: xy format where x=speed (0-F), y=depth (0-F)
local x = math.random(0, 15)
local y = math.random(0, 15)
return (x * 16) + y
elseif effect_command == "0R" then
-- Retrigger: xy format where x=volume factor (0-F), y=ticks (0-F)
local x = math.random(0, 15)
local y = math.random(1, 15) -- y shouldn't be 0 for retrigger
return (x * 16) + y
else
-- Default random value for other effects
return math.random(0, 255)
end
end
-- Generate Euclidean rhythm pattern
function PakettiFillGenerateEuclideanPattern(events, pattern_length)
if events <= 0 or pattern_length <= 0 then
return {}
end
if events >= pattern_length then
-- Fill all steps if events >= pattern_length
local pattern = {}
for i = 1, pattern_length do
pattern[i] = true
end
return pattern
end
-- Euclidean algorithm - distribute events evenly starting from step 1
local pattern = {}
for i = 1, pattern_length do
pattern[i] = false
end
-- Place events using the Euclidean distribution
local step_size = pattern_length / events
for i = 0, events - 1 do
local position = math.floor(i * step_size) + 1
if position <= pattern_length then
pattern[position] = true
end
end
return pattern
end
-- Get optimal From/To note range for current instrument in Euclidean mode
function PakettiFillGetOptimalNoteRange()
local song = renoise.song()
local instrument = song:instrument(song.selected_instrument_index)
if not instrument or #instrument.samples == 0 then
return 48, 60 -- Default C-4 to C-5 if no samples
end
-- Check if first sample has slice markers
local first_sample_has_slices = false
local slice_count = 0
local slice_start_note = nil
if #instrument.samples > 0 then
local first_sample = instrument:sample(1)
if first_sample and #first_sample.slice_markers > 0 then
first_sample_has_slices = true
slice_count = #first_sample.slice_markers + 1
-- Get the slice start note
local first_mapping = first_sample.sample_mapping
if first_mapping and first_mapping.note_range and #first_mapping.note_range >= 2 then
slice_start_note = first_mapping.note_range[1]
end
end
end
-- Read actual sample mappings to find mapped note ranges
local mapped_notes = {}
local start_sample_index = 1
-- If first sample has slices, include slice range and start from second sample for regular samples
if first_sample_has_slices then
-- Add slice notes to mapped range
if slice_start_note then
for i = 0, slice_count - 1 do
local note = slice_start_note + i
if note <= 119 then -- Clamp to max valid note
mapped_notes[note] = true
end
end
end
-- Start regular sample scanning from second sample (if it exists)
if #instrument.samples > 1 then
start_sample_index = 2
else
start_sample_index = #instrument.samples + 1 -- Skip regular sample loop
end
end
-- Scan through regular samples and their mappings
for sample_index = start_sample_index, #instrument.samples do
local sample = instrument:sample(sample_index)
-- Get the sample mapping for this sample
local mapping = sample.sample_mapping
if mapping and mapping.note_range and #mapping.note_range >= 2 then
-- Add all notes in this mapping's range
for note = mapping.note_range[1], mapping.note_range[2] do
mapped_notes[note] = true
end
end
end
-- Find the lowest and highest mapped notes
local min_note = nil
local max_note = nil
for note = 0, 119 do -- C-0 to B-9
if mapped_notes[note] then
if min_note == nil then
min_note = note
end
max_note = note
end
end
-- Return actual mapped range, or default if nothing found (clamp to max 119)
if min_note and max_note then
return min_note, math.min(119, max_note)
else
return 48, 60 -- Default C-4 to C-5 if no mappings found
end
end
-- Get sample name for note value from current instrument (truncated to 15 chars)
function PakettiFillGetSampleName(note_value)
local song = renoise.song()
local instrument = song:instrument(song.selected_instrument_index)
if not instrument or #instrument.samples == 0 then
local note_name, octave = PakettiFillValueToNote(note_value)
return string.format("%s%d", note_name, octave)
end
-- Check if first sample has slice markers
local first_sample_has_slices = false
local slice_count = 0
local slice_start_note = nil
if #instrument.samples > 0 then
local first_sample = instrument:sample(1)
if first_sample and #first_sample.slice_markers > 0 then
first_sample_has_slices = true
slice_count = #first_sample.slice_markers + 1
-- Get the slice start note
local first_mapping = first_sample.sample_mapping
if first_mapping and first_mapping.note_range and #first_mapping.note_range >= 2 then
slice_start_note = first_mapping.note_range[1]
end
end
end
-- PRIORITY 1: Check for sliced sample first
if first_sample_has_slices and slice_start_note then
-- Check if this note falls within the actual slice range
local slice_end_note = slice_start_note + slice_count - 1
if note_value >= slice_start_note and note_value <= slice_end_note then
local slice_index = note_value - slice_start_note + 1
return string.format("slice%02d", slice_index)
end
end
-- PRIORITY 2: Find which regular sample is mapped to this note
for sample_index = 1, #instrument.samples do
local sample = instrument:sample(sample_index)
-- Skip the first sample if it has slices (regardless of how many samples we have)
if not (first_sample_has_slices and sample_index == 1) then
local mapping = sample.sample_mapping
if mapping and mapping.note_range and #mapping.note_range >= 2 then
if note_value >= mapping.note_range[1] and note_value <= mapping.note_range[2] then
-- Found a regular sample mapping
if sample.name and sample.name ~= "" then
local name = sample.name
if #name > 9 then
name = string.sub(name, 1, 9) .. ".."
end
return name
else
return "Sample"
end
end
end
end
end
-- PRIORITY 3: If no sample mapping found, return note name
local note_name, octave = PakettiFillValueToNote(note_value)
return string.format("%s%d", note_name, octave)
end
-- Check Where? conditions (Polyend-style logic)
function PakettiFillShouldFillLine(pattern_line, where_mode, line_index, step_interval, selection, density, euclidean_pattern, remembered_fx_lines)
-- 1=Note, 2=No Note, 3=FX, 4=No FX, 5=Random, 6=Each, 7=Euclidean
-- If we have remembered FX pattern and we're in FX mode, use the remembered pattern
if where_mode == 3 and remembered_fx_lines then
local should_fill = remembered_fx_lines[line_index] == true
print("DEBUG PakettiFill: Using remembered pattern for line " .. line_index .. " -> " .. tostring(should_fill))
return should_fill
end
if where_mode == 1 then -- Note: only fill where notes exist
for i = 1, #pattern_line.note_columns do
if pattern_line.note_columns[i].note_value ~= renoise.PatternLine.EMPTY_NOTE then
return true
end
end
return false
elseif where_mode == 2 then -- No Note: only fill empty note slots
for i = 1, #pattern_line.note_columns do
if pattern_line.note_columns[i].note_value ~= renoise.PatternLine.EMPTY_NOTE then
return false
end
end
return true
elseif where_mode == 3 then -- FX: only fill where effects exist
for i = 1, #pattern_line.effect_columns do
if not pattern_line.effect_columns[i].is_empty then
return true
end
end
return false
elseif where_mode == 4 then -- No FX: only fill empty effect slots
for i = 1, #pattern_line.effect_columns do
if not pattern_line.effect_columns[i].is_empty then
return false
end
end
return true
elseif where_mode == 5 then -- Random: use density
return math.random(100) <= density
elseif where_mode == 6 then -- Each: step intervals
local relative_line = line_index - selection.start_line
local edit_step = renoise.song().transport.edit_step
return (relative_line % edit_step) == 0
elseif where_mode == 7 then -- Euclidean: use pattern starting from row 00
if euclidean_pattern and #euclidean_pattern > 0 then
local relative_line = line_index - selection.start_line -- 0-based from start of selection
local pattern_index = (relative_line % #euclidean_pattern) + 1 -- 1-based Lua index
return euclidean_pattern[pattern_index]
end
return false
end
return false
end
-- Apply fill to pattern selection
function PakettiFillApplyFill(density, fill_type, from_note, to_note, constant_note, use_random_fx, effects_only, selected_effect, where_mode, step_interval, selected_effect_index, effect_min_value, effect_max_value, use_editstep, remembered_fx_lines)
trueRandomSeed()
local song = renoise.song()
local selection = song.selection_in_pattern
print("DEBUG PakettiFill: Starting fill with parameters:")
print(" density=" .. density .. ", fill_type=" .. fill_type)
print(" use_random_fx=" .. tostring(use_random_fx) .. ", effects_only=" .. tostring(effects_only))
print(" where_mode=" .. where_mode .. ", selected_effect_index=" .. selected_effect_index)
print(" effect_min_value=" .. effect_min_value .. ", effect_max_value=" .. effect_max_value)
if selected_effect then
print(" selected_effect=" .. (selected_effect[1] or "nil"))
else
print(" selected_effect=nil")
end
if not selection then
renoise.app():show_status("No pattern selection found")
return
end
local available_effects = PakettiFillGetAvailableEffects()
-- Generate Euclidean pattern if needed
local euclidean_pattern = nil
if where_mode == 7 then -- Euclidean
-- density = events (number of hits)
local events = math.max(1, density)
-- Use EditStep or Step Length based on checkbox
local step_length
if use_editstep then
step_length = renoise.song().transport.edit_step
-- Treat EditStep = 0 as EditStep = 1 (step every line)
if step_length == 0 then
step_length = 1
print("DEBUG PakettiFill: Euclidean mode using EditStep = 0, treating as 1 (step every line)")
else
print("DEBUG PakettiFill: Euclidean mode using EditStep = " .. step_length)
end
else
step_length = math.max(1, step_interval)
print("DEBUG PakettiFill: Euclidean mode using Step Length = " .. step_length)
end
euclidean_pattern = PakettiFillGenerateEuclideanPattern(events, step_length)
end
-- Iterate through selection
for track_index = selection.start_track, selection.end_track do
local track = song:track(track_index)
if track.type == renoise.Track.TRACK_TYPE_SEQUENCER then
local note_columns_visible = track.visible_note_columns
local effect_columns_visible = track.visible_effect_columns
-- Ensure at least one note column is visible (unless effects only)
if not effects_only and note_columns_visible == 0 then
track.visible_note_columns = 1
note_columns_visible = 1
end
-- Ensure effect columns are visible if using random FX
print("DEBUG PakettiFill: Track " .. track_index .. " - use_random_fx=" .. tostring(use_random_fx) .. ", effect_columns_visible=" .. effect_columns_visible)
if use_random_fx and effect_columns_visible == 0 then
print("DEBUG PakettiFill: Making effect column visible for track " .. track_index)
track.visible_effect_columns = 1
effect_columns_visible = 1
end
local start_column = (track_index == selection.start_track) and selection.start_column or 1
local end_column = (track_index == selection.end_track) and selection.end_column or note_columns_visible
-- Process lines
print("DEBUG PakettiFill: Processing lines " .. selection.start_line .. " to " .. selection.end_line .. " for track " .. track_index)
for line_index = selection.start_line, selection.end_line do
local pattern_line = song:pattern(song.selected_pattern_index):track(track_index):line(line_index)
-- Check if this line should be filled based on Where? conditions
local should_fill = PakettiFillShouldFillLine(pattern_line, where_mode, line_index, step_interval, selection, density, euclidean_pattern, remembered_fx_lines)
print("DEBUG PakettiFill: Line " .. line_index .. " should_fill=" .. tostring(should_fill) .. " (density=" .. density .. "%)")
if should_fill then
-- Determine what to write based on where_mode and effects_only
local write_notes = true
local write_effects = true
print("DEBUG PakettiFill: Line " .. line_index .. " - effects_only=" .. tostring(effects_only) .. ", where_mode=" .. where_mode)
if effects_only then
-- Effects Only checkbox: only effects, no notes
write_notes = false
write_effects = true
print("DEBUG PakettiFill: Effects Only mode - write_notes=false, write_effects=true")
elseif where_mode == 3 then -- "FX" mode: only effects
write_notes = false
write_effects = true
print("DEBUG PakettiFill: FX mode - write_notes=false, write_effects=true")
elseif where_mode == 4 then -- "No FX" mode: only notes
write_notes = true
write_effects = false
print("DEBUG PakettiFill: No FX mode - write_notes=true, write_effects=false")
else
-- All other modes: both notes and effects
write_notes = true
write_effects = true
print("DEBUG PakettiFill: Standard mode - write_notes=true, write_effects=true")
end
-- Write notes if allowed
if write_notes then
for col = start_column, math.min(end_column, note_columns_visible) do
local note_column = pattern_line:note_column(col)
-- Generate note based on fill type
local note_value
if fill_type == 1 then -- Constant
if where_mode == 7 then -- Euclidean: ensure constant note is mapped
local mapped_notes = PakettiFillGetMappedNotes()
-- Check if constant_note is in mapped notes
local is_mapped = false
for _, mapped_note in ipairs(mapped_notes) do
if mapped_note == constant_note then
is_mapped = true
break
end
end
-- Use constant note if mapped, otherwise use first mapped note
note_value = is_mapped and constant_note or mapped_notes[1]
else
note_value = constant_note
end
elseif fill_type == 2 then -- From-To (distributed evenly)
if where_mode == 7 then -- Euclidean: distribute across mapped notes
local range_size = selection.end_line - selection.start_line
local line_position = line_index - selection.start_line
local ratio = range_size > 0 and line_position / range_size or 0
note_value = PakettiFillGenerateDistributedMappedNote(ratio)
else
-- Calculate position in range based on line position
local range_size = selection.end_line - selection.start_line
local line_position = line_index - selection.start_line
local ratio = range_size > 0 and line_position / range_size or 0
note_value = math.floor(from_note + (ratio * (to_note - from_note)))
end
else -- Random
if where_mode == 7 then -- Euclidean: use actually mapped notes
note_value = PakettiFillGenerateRandomMappedNote()
else
note_value = PakettiFillGenerateRandomNote(from_note, to_note)
end
end
-- Set the note (clamp to valid range)
note_value = math.max(0, math.min(119, note_value))
note_column.note_value = note_value
note_column.instrument_value = song.selected_instrument_index - 1
end
end
-- Write effects if allowed
if write_effects and effect_columns_visible > 0 then
print("DEBUG PakettiFill: Writing effects - write_effects=" .. tostring(write_effects) .. ", effect_columns_visible=" .. effect_columns_visible)
local effect_column = pattern_line:effect_column(1)
if effect_column then
print("DEBUG PakettiFill: Got effect column for track " .. track_index .. ", line " .. line_index)
else
print("DEBUG PakettiFill: ERROR - Could not get effect column 1 for track " .. track_index .. ", line " .. line_index)
print("DEBUG PakettiFill: Track effect_columns_visible=" .. track.visible_effect_columns)
end
if effect_column then
local effect_to_use
if use_random_fx and #available_effects > 0 then
-- Random FX takes precedence over dropdown selection
effect_to_use = available_effects[math.random(#available_effects)]
print("DEBUG PakettiFill: Using random FX: " .. (effect_to_use and effect_to_use[1] or "nil"))
elseif selected_effect_index == 1 then
-- "<No Effect>" selected and Random FX is off: clear effect
print("DEBUG PakettiFill: Clearing effect (No Effect selected)")
effect_column.number_string = ".."
effect_column.amount_value = 0
effect_to_use = nil -- Don't process further
elseif selected_effect then
-- Specific effect selected
effect_to_use = selected_effect
print("DEBUG PakettiFill: Using selected effect: " .. (effect_to_use and effect_to_use[1] or "nil"))
end
if effect_to_use then
print("DEBUG PakettiFill: About to write effect " .. effect_to_use[1])
effect_column.number_string = effect_to_use[1]
print("DEBUG PakettiFill: Set effect number to " .. effect_to_use[1])
-- Use min/max range for effect values if specific effect is selected
local effect_value
if not use_random_fx and selected_effect then
local min_val = math.min(effect_min_value, effect_max_value)
local max_val = math.max(effect_min_value, effect_max_value)
effect_value = math.random(min_val, max_val)
print("DEBUG PakettiFill: Using min/max range " .. min_val .. "-" .. max_val .. ", generated value " .. effect_value)
else
effect_value = PakettiFillGenerateEffectValue(effect_to_use[1])
print("DEBUG PakettiFill: Generated effect value " .. effect_value .. " using PakettiFillGenerateEffectValue")
end
effect_column.amount_value = effect_value
print("DEBUG PakettiFill: Successfully wrote effect " .. effect_to_use[1] .. string.format("%02X", effect_value) .. " to track " .. track_index .. ", line " .. line_index)
end
end
end
end
end
end
end
local mode_text = effects_only and "effects-only" or "notes"
renoise.app():show_status(string.format("PakettiFill: Applied %d%% density %s fill to selection", density, mode_text))
end
-- Toggle effect blacklist status
function PakettiFillToggleEffectBlacklist(effect_index)
effects_list[effect_index][4] = not effects_list[effect_index][4]
end
-- Global state variables (persist across dialog rebuilds)
local paketti_fill_density_value = 50
local paketti_fill_fill_type = 1 -- 1=Constant, 2=From-To, 3=Random
local paketti_fill_from_note_value = 48 -- C-4
local paketti_fill_to_note_value = 72 -- C-6 (safer default within B-9 range)
local paketti_fill_constant_note_name = "C-"
local paketti_fill_constant_octave = 4
local paketti_fill_use_random_fx = false
local paketti_fill_effects_only = false
local paketti_fill_show_effects_config = false
local paketti_fill_selected_effect_index = 1
local paketti_fill_where_mode = 1 -- 1=Note, 2=No Note, 3=FX, 4=No FX, 5=Random, 6=Each
local paketti_fill_step_interval = 8 -- For "Each" and Euclidean modes
local paketti_fill_effect_min_value = 0 -- Min parameter value (00-FF)
local paketti_fill_effect_max_value = 255 -- Max parameter value (00-FF)
local paketti_fill_use_editstep = true -- Use EditStep instead of Step Length slider in Euclidean mode
-- Create the main dialog
function PakettiFillShowDialog()
if dialog and dialog.visible then
dialog:close()
return
end
vb = renoise.ViewBuilder()
-- Use global state variables
local density_value = paketti_fill_density_value
local fill_type = paketti_fill_fill_type
local from_note_value = paketti_fill_from_note_value
local to_note_value = paketti_fill_to_note_value
local constant_note_name = paketti_fill_constant_note_name
local constant_octave = paketti_fill_constant_octave
local use_random_fx = paketti_fill_use_random_fx
local effects_only = paketti_fill_effects_only
local show_effects_config = paketti_fill_show_effects_config
local selected_effect_index = paketti_fill_selected_effect_index
local where_mode = paketti_fill_where_mode
local step_interval = paketti_fill_step_interval
local effect_min_value = paketti_fill_effect_min_value
local effect_max_value = paketti_fill_effect_max_value
local use_editstep = paketti_fill_use_editstep
-- Function to update effects only checkbox (declare before use)
local update_effects_only_checkbox = nil
-- Density slider (vertical)
local density_label = vb:text{
text = "Density",
style = "strong",
font = "bold"
}
local density_value_text = vb:text{
text = string.format("%d%%", density_value),
style = "strong",
font = "bold"
}
local density_slider = vb:slider{
min = 0,
max = 100,
value = density_value,
width = 80, -- Match other sliders for consistency
height = 200,
steps = {1, 10}, -- Small step: 1%, Big step: 10%
notifier = function(value)
density_value = math.floor(value)
paketti_fill_density_value = density_value -- Save to global
if where_mode == 6 or where_mode == 7 then -- Each or Euclidean
density_value_text.text = tostring(density_value)
else
density_value_text.text = string.format("%d%%", density_value)
end
end
}
-- Note range sliders (vertical) - declared first so they can be referenced
local from_label = vb:text{
text = "From",
style = "strong",
font = "bold"
}
local from_note_text = vb:text{
text = string.format("%s%d", PakettiFillValueToNote(from_note_value)),
style = "strong",
font = "bold",
}
local from_note_slider = vb:slider{
min = 0,
max = 119, -- B-9 is the highest note in Renoise
value = from_note_value,
width = 80, -- Match From Sample text width
height = 200,
steps = {1, 12}, -- Small step: 1 semitone, Big step: 1 octave
notifier = function(value)
from_note_value = math.min(119, math.floor(value)) -- Clamp to max 119
paketti_fill_from_note_value = from_note_value -- Save to global
if where_mode == 7 then -- Euclidean: show sample names
from_note_text.text = PakettiFillGetSampleName(from_note_value)
else
local note_name, octave = PakettiFillValueToNote(from_note_value)
from_note_text.text = string.format("%s%d", note_name, octave)
end
end
}
local to_label = vb:text{
text = "To",
style = "strong",
font = "bold"
}
local to_note_text = vb:text{
text = string.format("%s%d", PakettiFillValueToNote(to_note_value)),
style = "strong",
font = "bold",
}
local to_note_slider = vb:slider{
min = 0,
max = 119, -- B-9 is the highest note in Renoise
value = to_note_value,
width = 80, -- Match To Sample text width
height = 200,
steps = {1, 12}, -- Small step: 1 semitone, Big step: 1 octave
active = false, -- Start disabled since we're in Constant mode
notifier = function(value)
to_note_value = math.min(119, math.floor(value)) -- Clamp to max 119
paketti_fill_to_note_value = to_note_value -- Save to global
if where_mode == 7 then -- Euclidean: show sample names
to_note_text.text = PakettiFillGetSampleName(to_note_value)
else
local note_name, octave = PakettiFillValueToNote(to_note_value)
to_note_text.text = string.format("%s%d", note_name, octave)
end
end
}
-- Fill type selection (dropdown) - declared after sliders so it can reference them
local fill_type_text = vb:text{
text = "Fill Type",
style = "strong",
font = "bold"
}
local fill_type_popup = vb:popup{
items = {"Constant", "From-To", "Random"},
value = fill_type,
width = 120,
notifier = function(value)
fill_type = value
paketti_fill_fill_type = value -- Save to global
-- Enable/disable To slider based on fill type
if fill_type == 1 then -- Constant
to_note_slider.active = false
else -- From-To or Random
to_note_slider.active = true
end
end
}
-- Step interval control (for Euclidean mode)
local step_interval_text = vb:text{
text = (where_mode == 7) and "Step Length" or "Step",
style = "strong",
font = "bold"
}
local step_interval_value_text = vb:text{
text = tostring(step_interval),
style = "strong",
font = "bold"
}
local step_interval_slider = vb:slider{
min = 1,
max = 16, -- Reasonable max for Euclidean step lengths
value = step_interval,
width = 80, -- Match Step Length text width
height = 200,
steps = {1, 4}, -- Small step: 1, Big step: 4
active = (where_mode == 7 and not use_editstep), -- Active when Euclidean mode and not using EditStep
notifier = function(value)
step_interval = math.floor(value)
paketti_fill_step_interval = step_interval -- Save to global
step_interval_value_text.text = tostring(step_interval)
end
}
-- EditStep checkbox for Euclidean mode
local use_editstep_checkbox = vb:checkbox{
value = use_editstep,
notifier = function(value)
use_editstep = value
paketti_fill_use_editstep = value -- Save to global
-- Update slider active state and text display
if where_mode == 7 then -- Euclidean mode
step_interval_slider.active = not use_editstep
if use_editstep then
local current_editstep = renoise.song().transport.edit_step
-- Treat EditStep = 0 as 1 for display purposes
local display_editstep = (current_editstep == 0) and 1 or current_editstep
step_interval_value_text.text = tostring(display_editstep)
step_interval_text.text = "EditStep"
else
step_interval_value_text.text = tostring(step_interval)
step_interval_text.text = "Step Length"
end
end
end
}
-- Function to update EditStep display and sample names
local function PakettiFillUpdateRealTimeValues()
if dialog and dialog.visible then
-- Update EditStep display if using EditStep (in any relevant mode)
if use_editstep then
local current_editstep = renoise.song().transport.edit_step
-- Treat EditStep = 0 as 1 for display purposes
local display_editstep = (current_editstep == 0) and 1 or current_editstep
if where_mode == 7 then -- Euclidean mode
step_interval_value_text.text = tostring(display_editstep)
elseif where_mode == 6 then -- Each mode
step_interval_text.text = "EditStep: " .. tostring(display_editstep)
step_interval_value_text.text = tostring(display_editstep)
else
-- For other modes that use EditStep, update the step interval display
-- Note: In modes 1-5, the step interval might represent EditStep conceptually
if step_interval_value_text then
step_interval_value_text.text = tostring(display_editstep)
end
end
end
-- Update sample names in Euclidean mode if instrument changed
if where_mode == 7 then
local new_from_name = PakettiFillGetSampleName(from_note_value)
local new_to_name = PakettiFillGetSampleName(to_note_value)
if from_note_text.text ~= new_from_name then
from_note_text.text = new_from_name
end
if to_note_text.text ~= new_to_name then
to_note_text.text = new_to_name
end
end
end
end
-- Add real-time update using idle notifier for frequent updates
if not renoise.tool().app_idle_observable:has_notifier(PakettiFillUpdateRealTimeValues) then
renoise.tool().app_idle_observable:add_notifier(PakettiFillUpdateRealTimeValues)
end
-- Where? selection (Polyend-style) - declared after all referenced elements
local where_text = vb:text{
text = "Where?",
style = "strong",
font = "bold"
}
local where_popup = vb:popup{
items = {"Note", "No Note", "FX", "No FX", "Random", "Each", "Euclidean"},
value = where_mode,
width = 120,
notifier = function(value)
where_mode = value
paketti_fill_where_mode = value -- Save to global
-- Auto-check "FX Only" checkbox when "FX" or "No FX" is selected
if where_mode == 3 or where_mode == 4 then -- "FX" or "No FX"
effects_only = true
paketti_fill_effects_only = true
if update_effects_only_checkbox then
update_effects_only_checkbox() -- Update checkbox after variables are set
end
end
-- Update UI based on mode
if where_mode == 6 then -- Each (uses transport.edit_step)
density_label.text = "Density"
density_slider.max = 100
density_value_text.text = string.format("%d%%", density_value)
local current_editstep = renoise.song().transport.edit_step
-- Treat EditStep = 0 as 1 for display purposes
local display_editstep = (current_editstep == 0) and 1 or current_editstep
step_interval_text.text = "EditStep: " .. tostring(display_editstep)
step_interval_value_text.text = tostring(display_editstep)
step_interval_slider.active = false -- Disable slider for Each mode
from_label.text = "From"
to_label.text = "To"
-- Reset sliders to full range when switching from Euclidean mode
from_note_slider.min = 0
from_note_slider.max = 119
to_note_slider.min = 0
to_note_slider.max = 119
-- Reset width to allow auto-sizing for note names
from_note_text.width = 1
to_note_text.width = 1
-- Update text displays to show note names
local from_note_name, from_octave = PakettiFillValueToNote(from_note_value)
from_note_text.text = string.format("%s%d", from_note_name, from_octave)
local to_note_name, to_octave = PakettiFillValueToNote(to_note_value)
to_note_text.text = string.format("%s%d", to_note_name, to_octave)
elseif where_mode == 7 then -- Euclidean
-- Auto-switch from Constant to Random for better Euclidean patterns
if fill_type == 1 then -- If currently Constant
fill_type = 3 -- Change to Random
paketti_fill_fill_type = fill_type -- Save to global
fill_type_popup.value = fill_type
-- Enable To slider since we're no longer in Constant mode
to_note_slider.active = true
end
-- Set optimal From/To range for available samples
local optimal_from, optimal_to = PakettiFillGetOptimalNoteRange()
from_note_value = math.min(119, optimal_from)
to_note_value = math.min(119, optimal_to)
paketti_fill_from_note_value = from_note_value
paketti_fill_to_note_value = to_note_value
from_note_slider.value = from_note_value
to_note_slider.value = to_note_value
density_label.text = "Events"