-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathPakettiAutoSamplify.lua
More file actions
1676 lines (1451 loc) · 74.8 KB
/
PakettiAutoSamplify.lua
File metadata and controls
1676 lines (1451 loc) · 74.8 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
-----------------------------------------------------------------------
-- Automatic Sample Loader Settings Application System
-- Monitors instruments and applies Paketti loader settings when new samples are detected
-----------------------------------------------------------------------
-- Global state tracking for selected sample slot only
local previous_selected_sample_state = nil
local monitoring_enabled = true -- Will be initialized from preferences on startup
-- Global state tracking for all instruments and samples
local previous_instrument_states = {}
local previous_sample_counts = {}
-- Track samples that were just created by AutoSamplify to prevent loops
local recently_created_samples = {}
-- Simple approach: track files being loaded and process them once
local files_being_loaded = {}
local loaded_files_tracker = {}
local last_instrument_count = 0
-- Re-entrancy guard: prevents the monitoring timer from triggering a second
-- processing pass while AutoSamplify is already creating/copying instruments.
-- Also used as a "processing" flag so PakettiCheckForNewSamplesComprehensive
-- skips mid-batch: timer fires every 100 ms and instrument creation can take
-- longer than that if pakettiPreferencesDefaultInstrumentLoader is slow.
local autosamplify_processing = false
-- Debug flag: set to true during development to enable verbose logging.
-- Leave false in production to avoid flooding the Renoise scripting terminal
-- with hundreds of DEBUG lines every 100 ms.
local DEBUG_AUTOSAMPLIFY = false
local function dbg(...)
if DEBUG_AUTOSAMPLIFY then print(...) end
end
-- Ordered key list for loaded_files_tracker so we can rotate (evict oldest)
-- rather than wipe the entire table when it grows beyond MAX_TRACKER_SIZE.
-- A full wipe creates a re-processing window: the next timer tick sees all
-- previously-processed samples as "new" again.
local loaded_files_tracker_order = {}
local MAX_TRACKER_SIZE = 200
-- Helper: record a file as processed using its name|framecount dedup key.
-- Maintains loaded_files_tracker_order for ordered eviction.
local function track_file(key)
if not loaded_files_tracker[key] then
loaded_files_tracker[key] = true
table.insert(loaded_files_tracker_order, key)
end
end
-----------------------------------------------------------------------
-- Helper Functions for Copying Instrument Data (FX Chains, Modulation, Phrases)
-----------------------------------------------------------------------
-- Copy Sample Device Chains (Sample FX) from source instrument to target instrument
-- Returns a mapping table: source_chain_index -> target_chain_index
function PakettiCopySampleDeviceChains(source_instrument, target_instrument)
if not source_instrument or not target_instrument then
dbg("DEBUG: PakettiCopySampleDeviceChains - Invalid instruments provided")
return {}
end
local chain_mapping = {}
local source_chains = source_instrument.sample_device_chains
local target_chains_offset = #target_instrument.sample_device_chains
dbg(string.format("DEBUG: Copying %d FX chains from source (target has %d existing chains)",
#source_chains, target_chains_offset))
-- Copy each source chain
for source_chain_idx = 1, #source_chains do
local source_chain = source_chains[source_chain_idx]
-- Create new chain in target instrument
local new_chain_idx = target_chains_offset + source_chain_idx
target_instrument:insert_sample_device_chain_at(new_chain_idx)
local target_chain = target_instrument.sample_device_chains[new_chain_idx]
-- Copy chain name
target_chain.name = source_chain.name
-- Copy output routing if available
if source_chain.output_routing then
-- Check if the routing is available in the target
local routing_available = false
for _, available_routing in ipairs(target_chain.available_output_routings) do
if available_routing == source_chain.output_routing then
routing_available = true
break
end
end
if routing_available then
target_chain.output_routing = source_chain.output_routing
end
end
-- Copy devices (skip index 1 which is the mixer device - it's always present)
local source_devices = source_chain.devices
for device_idx = 2, #source_devices do
local source_device = source_devices[device_idx]
local device_path = source_device.device_path
if device_path and device_path ~= "" then
-- Check if device is available
local device_available = false
for _, available_device in ipairs(target_chain.available_devices) do
if available_device == device_path then
device_available = true
break
end
end
if device_available then
-- Insert the device at the end of the chain
local new_device_idx = #target_chain.devices + 1
local new_device = target_chain:insert_device_at(device_path, new_device_idx)
if new_device then
-- Copy device state using active_preset_data (XML)
local success, err = pcall(function()
new_device.active_preset_data = source_device.active_preset_data
end)
if not success then
dbg(string.format("DEBUG: Could not copy preset data for device '%s': %s",
source_device.name, tostring(err)))
-- Fallback: copy individual parameters
for param_idx = 1, #source_device.parameters do
local source_param = source_device.parameters[param_idx]
local target_param = new_device.parameters[param_idx]
if target_param and source_param then
pcall(function()
target_param.value = source_param.value
end)
end
end
end
-- Copy display settings
new_device.is_active = source_device.is_active
new_device.is_maximized = source_device.is_maximized
if source_device.display_name and source_device.display_name ~= "" then
new_device.display_name = source_device.display_name
end
dbg(string.format("DEBUG: Copied device '%s' to chain %d", source_device.name, new_chain_idx))
else
dbg(string.format("DEBUG: Failed to insert device '%s' at path '%s'",
source_device.name, device_path))
end
else
dbg(string.format("DEBUG: Device '%s' (path: %s) not available in target chain",
source_device.name, device_path))
end
end
end
-- Store the mapping
chain_mapping[source_chain_idx] = new_chain_idx
dbg(string.format("DEBUG: Mapped source chain %d -> target chain %d ('%s')",
source_chain_idx, new_chain_idx, source_chain.name))
end
return chain_mapping
end
-- Copy Sample Modulation Sets from source instrument to target instrument
-- Returns a mapping table: source_set_index -> target_set_index
function PakettiCopySampleModulationSets(source_instrument, target_instrument)
if not source_instrument or not target_instrument then
dbg("DEBUG: PakettiCopySampleModulationSets - Invalid instruments provided")
return {}
end
local modset_mapping = {}
local source_modsets = source_instrument.sample_modulation_sets
local target_modsets_offset = #target_instrument.sample_modulation_sets
dbg(string.format("DEBUG: Copying %d modulation sets from source (target has %d existing sets)",
#source_modsets, target_modsets_offset))
-- Copy each source modulation set
for source_set_idx = 1, #source_modsets do
local source_modset = source_modsets[source_set_idx]
-- Create new modulation set in target instrument
local new_set_idx = target_modsets_offset + source_set_idx
target_instrument:insert_sample_modulation_set_at(new_set_idx)
local target_modset = target_instrument.sample_modulation_sets[new_set_idx]
-- Use copy_from to copy all modulation set contents
local success, err = pcall(function()
target_modset:copy_from(source_modset)
end)
if success then
dbg(string.format("DEBUG: Copied modulation set %d -> %d ('%s')",
source_set_idx, new_set_idx, source_modset.name))
else
dbg(string.format("DEBUG: Error copying modulation set %d: %s", source_set_idx, tostring(err)))
-- Fallback: copy basic properties manually
target_modset.name = source_modset.name
if source_modset.filter_type then
target_modset.filter_type = source_modset.filter_type
end
if source_modset.pitch_range then
target_modset.pitch_range = source_modset.pitch_range
end
end
-- Store the mapping
modset_mapping[source_set_idx] = new_set_idx
end
return modset_mapping
end
-- Copy Phrases from source instrument to target instrument
function PakettiCopyPhrases(source_instrument, target_instrument)
if not source_instrument or not target_instrument then
dbg("DEBUG: PakettiCopyPhrases - Invalid instruments provided")
return
end
local source_phrases = source_instrument.phrases
if #source_phrases == 0 then
dbg("DEBUG: No phrases to copy from source instrument")
return
end
dbg(string.format("DEBUG: Copying %d phrases from source instrument", #source_phrases))
-- Copy each source phrase
for phrase_idx = 1, #source_phrases do
local source_phrase = source_phrases[phrase_idx]
-- Insert new phrase in target instrument
local new_phrase = target_instrument:insert_phrase_at(phrase_idx)
if new_phrase then
-- Use copy_from to copy phrase contents
local success, err = pcall(function()
new_phrase:copy_from(source_phrase)
end)
if success then
dbg(string.format("DEBUG: Copied phrase %d ('%s')", phrase_idx, source_phrase.name))
else
dbg(string.format("DEBUG: Error copying phrase %d: %s", phrase_idx, tostring(err)))
end
else
dbg(string.format("DEBUG: Failed to create phrase at index %d", phrase_idx))
end
end
-- Copy phrase mappings if any exist
if #source_instrument.phrase_mappings > 0 then
dbg(string.format("DEBUG: Source has %d phrase mappings (note: mappings are auto-created with phrases)",
#source_instrument.phrase_mappings))
end
-- Copy phrase playback mode
target_instrument.phrase_playback_mode = source_instrument.phrase_playback_mode
target_instrument.phrase_program = source_instrument.phrase_program
end
-- Helper function to check if source instrument has content worth copying
function PakettiSourceHasContentToCopy(source_instrument)
if not source_instrument then return false end
local has_fx_chains = #source_instrument.sample_device_chains > 0
local has_modulation = #source_instrument.sample_modulation_sets > 0
local has_phrases = #source_instrument.phrases > 0
-- Check if any sample actually uses the FX chains or modulation
local uses_fx = false
local uses_modulation = false
for _, sample in ipairs(source_instrument.samples) do
if sample.device_chain_index > 0 then
uses_fx = true
end
if sample.modulation_set_index > 0 then
uses_modulation = true
end
end
return (has_fx_chains and uses_fx) or (has_modulation and uses_modulation) or has_phrases
end
-----------------------------------------------------------------------
-- End of Helper Functions
-----------------------------------------------------------------------
-- AutoSamplify version of PakettiInjectApplyLoaderSettings (NO normalization)
function PakettiAutoSamplifyApplyLoaderSettings(sample)
if not sample or not preferences then return end
dbg(string.format("DEBUG: PakettiAutoSamplifyApplyLoaderSettings called for '%s'", sample.name))
-- Check if PCM Writer is currently creating samples - if so, skip AutoSamplify processing
if PCMWriterIsCreatingSamples and PCMWriterIsCreatingSamples() then
dbg(string.format("DEBUG: Skipping AutoSamplify processing for '%s' - PCM Writer is creating samples", sample.name))
return
end
-- Detect if this is a PCM Writer sample (they have specific naming patterns)
local is_pcm_writer_sample = string.find(sample.name, "^PCM ") ~= nil
-- Apply Paketti Loader preferences to the sample
sample.interpolation_mode = preferences.pakettiLoaderInterpolation.value
sample.oversample_enabled = preferences.pakettiLoaderOverSampling.value
sample.autofade = preferences.pakettiLoaderAutofade.value
sample.autoseek = preferences.pakettiLoaderAutoseek.value
sample.oneshot = preferences.pakettiLoaderOneshot.value
-- For PCM Writer samples, preserve their loop_mode and NNA (they set these explicitly)
if not is_pcm_writer_sample then
sample.loop_mode = preferences.pakettiLoaderLoopMode.value
sample.new_note_action = preferences.pakettiLoaderNNA.value
else
dbg(string.format("DEBUG: Preserving loop_mode and NNA for PCM Writer sample '%s'", sample.name))
end
sample.loop_release = preferences.pakettiLoaderLoopExit.value
-- NO normalization in AutoSamplify - just apply sample settings
print(string.format("Applied Paketti loader settings to sample: %s (no normalization)%s", sample.name, is_pcm_writer_sample and " - loop_mode and NNA preserved" or ""))
end
-- Function to get the current selected sample slot state
function PakettiGetSelectedSampleState()
local song = renoise.song()
if not song then
return { exists = false, has_data = false, instrument_index = nil, sample_index = nil }
end
local instrument_index = song.selected_instrument_index
local sample_index = song.selected_sample_index
if instrument_index < 1 or instrument_index > #song.instruments then
return { exists = false, has_data = false, instrument_index = instrument_index, sample_index = sample_index }
end
local instrument = song.instruments[instrument_index]
if not instrument then
return { exists = false, has_data = false, instrument_index = instrument_index, sample_index = sample_index }
end
-- Check if sample slot exists
local sample_exists = (sample_index >= 1 and sample_index <= #instrument.samples)
local has_sample_data = false
if sample_exists then
local sample = instrument.samples[sample_index]
has_sample_data = (sample.sample_buffer and sample.sample_buffer.has_sample_data)
end
return {
exists = sample_exists,
has_data = has_sample_data,
instrument_index = instrument_index,
sample_index = sample_index
}
end
-- Function to get state of all instruments and their samples
function PakettiGetAllInstrumentStates()
local song = renoise.song()
if not song then
return {}
end
local states = {}
for i = 1, #song.instruments do
local instrument = song.instruments[i]
local sample_states = {}
for j = 1, #instrument.samples do
local sample = instrument.samples[j]
local has_data = (sample.sample_buffer and sample.sample_buffer.has_sample_data)
sample_states[j] = {
exists = true,
has_data = has_data,
name = sample.name,
-- Snapshot is_slice_alias so PakettiFindNewlyLoadedSamples can filter
-- alias samples without needing a live song reference.
is_slice_alias = sample.is_slice_alias,
-- Frame count for richer deduplication: two files both named "Kick.wav"
-- from different folders are distinguished by their length in frames,
-- preventing false-positive deduplication of genuinely different samples.
frame_count = has_data and sample.sample_buffer.number_of_frames or 0
}
end
states[i] = {
instrument_index = i,
sample_count = #instrument.samples,
sample_states = sample_states,
name = instrument.name
}
end
return states
end
-- Simple function to find newly loaded samples by comparing states
function PakettiFindNewlyLoadedSamples(current_states, previous_states)
local new_samples = {}
-- Check each instrument
for i = 1, #current_states do
local current_instr = current_states[i]
local previous_instr = previous_states[i]
if not previous_instr then
-- New instrument - check if it was created by AutoSamplify.
-- Match by BOTH index and name: after a previous insertion shifts all
-- indices by 1, the index stored in recently_created_samples can be stale,
-- but the instrument_name we recorded at creation time remains correct.
local is_autosamplify_created = false
for _, created_sample in ipairs(recently_created_samples) do
if created_sample.instrument_index == i then
is_autosamplify_created = true
break
end
if created_sample.instrument_name and created_sample.instrument_name ~= ""
and current_instr.name == created_sample.instrument_name then
is_autosamplify_created = true
break
end
end
-- Only process if it wasn't created by AutoSamplify
if not is_autosamplify_created then
for j = 1, current_instr.sample_count do
local sample = current_instr.sample_states[j]
-- Skip slice alias samples: they are auto-created by Renoise from the
-- parent sample's slice markers and must never be processed separately.
if sample and sample.has_data and not sample.is_slice_alias then
-- Richer dedup key: name + frame count so two "Kick.wav" files from
-- different folders are not falsely collapsed into one tracker entry.
local sample_key = sample.name .. "|" .. tostring(sample.frame_count or 0)
-- Only process if we haven't seen this exact sample before
if not loaded_files_tracker[sample_key] then
table.insert(new_samples, {
instrument_index = i,
sample_index = j,
sample_name = sample.name,
dedup_key = sample_key
})
dbg(string.format("DEBUG: Found new sample: %s in instrument %d, slot %d", sample.name, i, j))
else
dbg(string.format("DEBUG: Skipping already processed sample: %s (already processed elsewhere)", sample.name))
end
end
end
else
dbg(string.format("DEBUG: Skipping new instrument %d - created by AutoSamplify", i))
end
else
-- Existing instrument - check for new samples
for j = 1, current_instr.sample_count do
local current_sample = current_instr.sample_states[j]
local previous_sample = previous_instr.sample_states[j]
-- Skip slice alias samples: auto-created by Renoise from slice markers on
-- the parent sample; processing them separately causes duplicates.
if current_sample and current_sample.has_data and not current_sample.is_slice_alias then
local is_new = false
if not previous_sample then
-- Sample slot didn't exist before
is_new = true
elseif not previous_sample.has_data and current_sample.has_data then
-- Sample slot was empty before, now has data
is_new = true
end
if is_new then
-- Check if this sample was just created by AutoSamplify
local is_autosamplify_created = false
for _, created_sample in ipairs(recently_created_samples) do
if created_sample.instrument_index == i and created_sample.sample_index == j then
is_autosamplify_created = true
break
end
end
if not is_autosamplify_created then
-- Richer dedup key: name + frame count so two "Kick.wav" files from
-- different folders are not falsely collapsed into one tracker entry.
local sample_key = current_sample.name .. "|" .. tostring(current_sample.frame_count or 0)
-- Only process if we haven't seen this exact sample before
if not loaded_files_tracker[sample_key] then
table.insert(new_samples, {
instrument_index = i,
sample_index = j,
sample_name = current_sample.name,
dedup_key = sample_key
})
dbg(string.format("DEBUG: Found new sample: %s in instrument %d, slot %d", current_sample.name, i, j))
else
dbg(string.format("DEBUG: Skipping already processed sample: %s (already processed elsewhere)", current_sample.name))
end
else
dbg(string.format("DEBUG: Skipping sample %d in instrument %d - created by AutoSamplify", j, i))
end
end
end
end
end
end
return new_samples
end
-- Function to check if instrument is already Pakettified
-- A "pakettified" instrument has the Volume AHDSR device present, regardless of whether it's active
function PakettiIsInstrumentPakettified(instrument)
-- Check for plugins
if instrument.plugin_properties and instrument.plugin_properties.plugin_device then
return true
end
-- Check for Volume AHDSR device using helper function (defined in another
-- module; guard against nil in case load order changes or it is renamed).
local _ahdsr_ok, _has_ahdsr = pcall(function()
return find_volume_ahdsr_device and find_volume_ahdsr_device(instrument)
end)
if _ahdsr_ok and _has_ahdsr then
return true
end
-- Check for macro assignments (if any macros are assigned to parameters)
for i = 1, 8 do
if instrument.macros[i] and #instrument.macros[i].mappings > 0 then
return true
end
end
return false
end
-- Function to apply Paketti loader settings to a specific sample
function PakettiApplyLoaderSettingsToSample(instrument_index, sample_index)
if not monitoring_enabled then return end
-- Check if we should skip automatic processing (e.g., when CTRL-O Pattern to Sample is handling it)
if PakettiDontRunAutomaticSampleLoader then return end
local song = renoise.song()
if not song then return end
if instrument_index < 1 or instrument_index > #song.instruments then
return
end
local source_instrument = song.instruments[instrument_index]
if not source_instrument or sample_index < 1 or sample_index > #source_instrument.samples then
return
end
local source_sample = source_instrument.samples[sample_index]
if not source_sample or not source_sample.sample_buffer or not source_sample.sample_buffer.has_sample_data then
return
end
-- Store sample data before processing
local sample_name = source_sample.name
-- Deduplication pre-check: if the 100 ms timer path already processed this
-- exact sample (same name + frame count), the legacy observable path must not
-- process it a second time. This is the primary guard against double-processing:
-- the timer finishes, clears autosamplify_processing, THEN the observable fires.
local _pre_fc = source_sample.sample_buffer.number_of_frames
local _pre_key = sample_name .. "|" .. tostring(_pre_fc)
if loaded_files_tracker[_pre_key] then
dbg("DEBUG: PakettiApplyLoaderSettingsToSample: '%s' already processed by timer path - skipping", sample_name)
return
end
local is_pakettified = PakettiIsInstrumentPakettified(source_instrument)
local has_other_samples = #source_instrument.samples > 1 or (sample_index > 1)
-- Mark this sample as processed BEFORE doing any work.
-- Use name + frame_count as key to distinguish two "Kick.wav" files from
-- different folders that happen to share the same filename.
local _fc = (source_sample.sample_buffer and source_sample.sample_buffer.has_sample_data)
and source_sample.sample_buffer.number_of_frames or 0
local _tracker_key = sample_name .. "|" .. tostring(_fc)
track_file(_tracker_key)
dbg("DEBUG: Marking sample '%s' (frames: %d) as processed", sample_name, _fc)
print(string.format("Processing sample '%s' from instrument %d, slot %d (pakettified: %s, has_other_samples: %s)",
sample_name, instrument_index, sample_index,
tostring(is_pakettified), tostring(has_other_samples)))
-- Check AutoSamplify Pakettify preference
local should_pakettify = true
if preferences and preferences.pakettiAutoSamplifyPakettify then
should_pakettify = preferences.pakettiAutoSamplifyPakettify.value
end
-- If Pakettify is Off, just apply sample settings and normalize in place
if not should_pakettify then
print("AutoSamplify Pakettify is OFF - applying sample settings and normalization in place")
PakettiAutoSamplifyApplyLoaderSettings(source_sample)
renoise.app():show_status(string.format("Applied sample settings to '%s' (Pakettify OFF)", sample_name))
return
end
-- If instrument is already pakettified and has other samples, just apply loader settings in place
if is_pakettified and has_other_samples then
print("Instrument already pakettified with other samples - applying loader settings in place")
PakettiAutoSamplifyApplyLoaderSettings(source_sample)
renoise.app():show_status(string.format("Applied Paketti settings to '%s' in existing pakettified instrument", sample_name))
return
end
-- Store source sample's FX chain and modulation set indices BEFORE creating new instrument
local source_device_chain_index = source_sample.device_chain_index
local source_modulation_set_index = source_sample.modulation_set_index
local source_has_content = PakettiSourceHasContentToCopy(source_instrument)
dbg(string.format("DEBUG: Source sample indices - device_chain: %d, modulation_set: %d, has_content: %s",
source_device_chain_index, source_modulation_set_index, tostring(source_has_content)))
-- Create new instrument after current one.
-- Wrapped in pcall so any mid-creation Renoise API error becomes a logged
-- warning rather than a hard crash that leaves monitoring permanently broken.
-- _instrument_was_created / _created_at_index: if the pcall body fails after
-- safeInsertInstrumentAt has already inserted a new slot, we delete it here so
-- it does not remain as an empty, half-populated orphan in the instrument list.
local _instrument_was_created = false
local _created_at_index = nil
local _create_ok, _create_err = pcall(function()
local new_instrument_index = instrument_index + 1
if not safeInsertInstrumentAt(song, new_instrument_index) then return end
_instrument_was_created = true
_created_at_index = new_instrument_index
-- Index drift correction: inserting at new_instrument_index shifts every
-- previously tracked instrument that sits at or above that index up by 1.
for _, _tracked in ipairs(recently_created_samples) do
if _tracked.instrument_index >= new_instrument_index then
_tracked.instrument_index = _tracked.instrument_index + 1
end
end
song.selected_instrument_index = new_instrument_index
-- Track the newly created instrument to prevent loops.
-- Store instrument_name (= sample_name) in addition to the index so the guard
-- in PakettiFindNewlyLoadedSamples can still identify this instrument even
-- after subsequent instrument insertions shift all indices by 1.
table.insert(recently_created_samples, {
instrument_index = new_instrument_index,
instrument_name = sample_name,
sample_index = 1,
created_by_autosamplify = true,
})
dbg(string.format("DEBUG: Tracked new instrument %d ('%s') as AutoSamplify-created", new_instrument_index, sample_name))
-- Apply the default XRNI settings to the new instrument
print(string.format("Loading default XRNI into new instrument %d", new_instrument_index))
pakettiPreferencesDefaultInstrumentLoader()
-- Verify the new instrument is still accessible.
-- pakettiPreferencesDefaultInstrumentLoader() can silently fail if no default XRNI
-- is configured, or can change selected_instrument_index in unexpected ways.
local new_instrument = song.instruments[new_instrument_index]
if not new_instrument then
print(string.format("ERROR: Expected instrument at index %d after loading XRNI, but got nil - aborting", new_instrument_index))
return
end
-- Store the number of default chains and modulation sets from the loaded XRNI
local default_chain_count = #new_instrument.sample_device_chains
local default_modset_count = #new_instrument.sample_modulation_sets
dbg(string.format("DEBUG: Default XRNI has %d FX chains and %d modulation sets",
default_chain_count, default_modset_count))
-- Copy FX chains, modulation sets, and phrases from source instrument if it has content
local chain_mapping = {}
local modset_mapping = {}
if source_has_content then
dbg("DEBUG: Source instrument has content to copy - copying FX chains, modulation sets, and phrases")
-- Copy Sample FX Chains from source (appending after default chains)
if #source_instrument.sample_device_chains > 0 then
chain_mapping = PakettiCopySampleDeviceChains(source_instrument, new_instrument)
end
-- Copy Sample Modulation Sets from source (appending after default sets)
if #source_instrument.sample_modulation_sets > 0 then
modset_mapping = PakettiCopySampleModulationSets(source_instrument, new_instrument)
end
-- Copy Phrases from source
if #source_instrument.phrases > 0 then
PakettiCopyPhrases(source_instrument, new_instrument)
end
end
-- Clear any slice markers from the default XRNI samples BEFORE deletion.
-- Renoise forbids delete_sample_at on an instrument that has sliced samples
-- (any sample with slice_markers), so we must clear those markers first.
-- (The batch path already does this; this brings the single-sample path in sync.)
for _si = 1, #new_instrument.samples do
local _s = new_instrument.samples[_si]
if _s and #_s.slice_markers > 0 then
local _snap = {}
for _, _m in ipairs(_s.slice_markers) do table.insert(_snap, _m) end
for _, _m in ipairs(_snap) do _s:delete_slice_marker(_m) end
dbg(string.format("DEBUG: Cleared %d slice markers from default-XRNI sample %d before deletion", #_snap, _si))
end
end
-- Clear default samples if they exist
if #new_instrument.samples > 0 then
for i = #new_instrument.samples, 1, -1 do
new_instrument:delete_sample_at(i)
end
end
-- Insert new sample slot and copy the sample data
new_instrument:insert_sample_at(1)
song.selected_sample_index = 1
local new_sample = new_instrument.samples[1]
-- Check if source sample is a slice alias - cannot use copy_from on slice alias samples
if source_sample.is_slice_alias then
dbg(string.format("DEBUG: Source sample '%s' is a slice alias, copying properties and buffer manually", sample_name))
-- Copy sample properties manually
new_sample.panning = source_sample.panning
new_sample.volume = source_sample.volume
new_sample.transpose = source_sample.transpose
new_sample.fine_tune = source_sample.fine_tune
new_sample.beat_sync_enabled = source_sample.beat_sync_enabled
new_sample.beat_sync_lines = source_sample.beat_sync_lines
pakettiSafeCopyBeatSyncMode(new_sample, source_sample)
new_sample.interpolation_mode = source_sample.interpolation_mode
new_sample.oversample_enabled = source_sample.oversample_enabled
new_sample.new_note_action = source_sample.new_note_action
new_sample.oneshot = source_sample.oneshot
new_sample.mute_group = source_sample.mute_group
new_sample.autoseek = source_sample.autoseek
new_sample.autofade = source_sample.autofade
new_sample.loop_mode = source_sample.loop_mode
new_sample.loop_release = source_sample.loop_release
-- Copy buffer data manually from alias sample
local source_buffer = source_sample.sample_buffer
local dest_buffer = new_sample.sample_buffer
if source_buffer.has_sample_data then
local success = dest_buffer:create_sample_data(
source_buffer.sample_rate,
source_buffer.bit_depth,
source_buffer.number_of_channels,
source_buffer.number_of_frames
)
if success then
-- Frame-by-frame copy is slow for large samples.
-- A 30-second stereo 44.1kHz sample requires ~2.6 million individual
-- Renoise API calls. Show a status message so the user knows Renoise is
-- working and hasn't frozen.
if source_buffer.number_of_frames > 44100 then
renoise.app():show_status(string.format(
"AutoSamplify: copying '%s' (%d frames) — please wait...",
sample_name, source_buffer.number_of_frames))
end
dest_buffer:prepare_sample_data_changes()
for ch = 1, source_buffer.number_of_channels do
for fr = 1, source_buffer.number_of_frames do
dest_buffer:set_sample_data(ch, fr, source_buffer:sample_data(ch, fr))
end
end
dest_buffer:finalize_sample_data_changes()
-- Copy loop points after buffer is created
if source_sample.loop_start <= source_buffer.number_of_frames then
new_sample.loop_start = source_sample.loop_start
end
if source_sample.loop_end <= source_buffer.number_of_frames then
new_sample.loop_end = source_sample.loop_end
end
dbg("DEBUG: Successfully copied alias sample buffer (%d frames, %d channels)",
source_buffer.number_of_frames, source_buffer.number_of_channels)
else
print(string.format("ERROR: Failed to create sample buffer for alias sample '%s'", sample_name))
end
end
else
-- Normal copy for non-alias samples
new_sample:copy_from(source_sample)
end
new_sample.name = sample_name
new_instrument.name = sample_name
-- Update sample's device_chain_index and modulation_set_index to point to copied chains/sets
if source_device_chain_index > 0 and chain_mapping[source_device_chain_index] then
new_sample.device_chain_index = chain_mapping[source_device_chain_index]
dbg(string.format("DEBUG: Updated sample device_chain_index: %d -> %d",
source_device_chain_index, new_sample.device_chain_index))
end
if source_modulation_set_index > 0 and modset_mapping[source_modulation_set_index] then
new_sample.modulation_set_index = modset_mapping[source_modulation_set_index]
dbg(string.format("DEBUG: Updated sample modulation_set_index: %d -> %d",
source_modulation_set_index, new_sample.modulation_set_index))
end
-- Apply sample-specific loader settings to the new sample
PakettiAutoSamplifyApplyLoaderSettings(new_sample)
local copied_content = ""
if source_has_content then
local parts = {}
if #chain_mapping > 0 then table.insert(parts, string.format("%d FX chains", #chain_mapping)) end
if #modset_mapping > 0 then table.insert(parts, string.format("%d mod sets", #modset_mapping)) end
if #source_instrument.phrases > 0 then table.insert(parts, string.format("%d phrases", #source_instrument.phrases)) end
if #parts > 0 then
copied_content = " + copied " .. table.concat(parts, ", ")
end
end
print(string.format("Successfully Pakettified '%s' in new instrument %d with XRNI + loader settings%s",
sample_name, new_instrument_index, copied_content))
renoise.app():show_status(string.format("Auto-Pakettified '%s' to new instrument %d%s",
sample_name, new_instrument_index, copied_content))
end) -- end pcall (instrument creation)
if not _create_ok then
print("ERROR: PakettiApplyLoaderSettingsToSample: instrument creation failed: " .. tostring(_create_err))
-- Attempt to delete the partially-created instrument so it does not remain as
-- an orphaned, half-populated slot in the instrument list.
if _instrument_was_created and _created_at_index then
local _cs = renoise.song()
if _cs and _created_at_index <= #_cs.instruments then
pcall(function()
_cs:delete_instrument_at(_created_at_index)
dbg(string.format("DEBUG: Cleaned up orphaned instrument at index %d after pcall failure", _created_at_index))
end)
end
end
renoise.app():show_status("AutoSamplify: instrument creation error - see Renoise log")
end
end
-- Function to apply Paketti loader settings to the selected sample (legacy compatibility)
function PakettiApplyLoaderSettingsToSelectedSample()
-- Set the re-entrancy flag so a concurrent 100 ms monitoring tick does not
-- treat the partially-created instrument as a new unprocessed sample.
if autosamplify_processing then
dbg("DEBUG: PakettiApplyLoaderSettingsToSelectedSample skipped - already processing")
return
end
autosamplify_processing = true
local _ok, _err = pcall(function()
local current_state = PakettiGetSelectedSampleState()
if current_state.exists and current_state.has_data then
PakettiApplyLoaderSettingsToSample(current_state.instrument_index, current_state.sample_index)
end
end)
if not _ok then
print("ERROR: PakettiApplyLoaderSettingsToSelectedSample: " .. tostring(_err))
renoise.app():show_status("AutoSamplify: error applying settings - see Renoise log")
end
autosamplify_processing = false
end
-- Function to apply settings to multiple newly loaded samples
function PakettiApplyLoaderSettingsToNewSamples(new_samples)
if not monitoring_enabled then return end
-- Check if we should skip automatic processing
if PakettiDontRunAutomaticSampleLoader then return end
if #new_samples == 0 then return end
-- Re-entrancy guard: if AutoSamplify is already creating instruments, skip this
-- call. The monitoring timer fires every 100 ms and instrument creation can take
-- longer (pakettiPreferencesDefaultInstrumentLoader is blocking), so a second
-- invocation can arrive while the first is still running.
if autosamplify_processing then
dbg("DEBUG: AutoSamplify re-entrancy detected - skipping nested invocation")
return
end
autosamplify_processing = true
-- Track all instruments created during this batch so any that were fully or
-- partially created before a pcall failure can be deleted (orphan prevention).
local _batch_created_indices = {}
-- Wrap entire batch in pcall so any mid-batch Renoise API error:
-- (a) produces a clear log message rather than a silent crash, and
-- (b) always resets the re-entrancy flag so future timer ticks run normally.
local _processing_ok, _processing_err = pcall(function()
print(string.format("Processing %d newly loaded samples", #new_samples))
-- Check AutoSamplify Pakettify preference
local should_pakettify = true
if preferences and preferences.pakettiAutoSamplifyPakettify then
should_pakettify = preferences.pakettiAutoSamplifyPakettify.value
end
local song = renoise.song()
if not song then return end
-- Group samples by instrument for batch processing
local samples_by_instrument = {}
for _, sample_info in ipairs(new_samples) do
local instr_idx = sample_info.instrument_index
if not samples_by_instrument[instr_idx] then
samples_by_instrument[instr_idx] = {}
end
table.insert(samples_by_instrument[instr_idx], sample_info)
end
-- Process each instrument's new samples.
-- Iterate in DESCENDING index order so that when we insert a new instrument
-- at instr_idx + 1, the shift only affects higher indices that have already
-- been processed -- lower indices remain stable for subsequent iterations.
local _sorted_instr_indices = {}
for _idx in pairs(samples_by_instrument) do table.insert(_sorted_instr_indices, _idx) end
table.sort(_sorted_instr_indices, function(a, b) return a > b end)
for _, instr_idx in ipairs(_sorted_instr_indices) do
local samples = samples_by_instrument[instr_idx]
local instrument = song.instruments[instr_idx]
if instrument then
local is_pakettified = PakettiIsInstrumentPakettified(instrument)
local total_samples = #instrument.samples
local new_sample_count = #samples
-- Check if this instrument has samples that were loaded from external sources
-- (not created by AutoSamplify)
local has_external_samples = false
for i = 1, total_samples do
local sample = instrument.samples[i]
if sample and sample.sample_buffer.has_sample_data then
-- Check if this sample is not in our recently created list
local is_autosamplify_created = false
for _, created_sample in ipairs(recently_created_samples) do
if created_sample.instrument_index == instr_idx and created_sample.sample_index == i then
is_autosamplify_created = true
break
end
end
if not is_autosamplify_created then
has_external_samples = true
break
end
end
end
if not should_pakettify then
-- Pakettify OFF: Apply settings to all samples in place
print(string.format("Pakettify OFF: Applying settings to %d samples in instrument %d", new_sample_count, instr_idx))
for _, sample_info in ipairs(samples) do
local sample = instrument.samples[sample_info.sample_index]
if sample then
-- Mark as processed before applying settings (name|frames key)
local _fc = (sample.sample_buffer and sample.sample_buffer.has_sample_data)
and sample.sample_buffer.number_of_frames or 0
track_file(sample.name .. "|" .. tostring(_fc))
dbg(string.format("DEBUG: Marking sample '%s' as processed (Pakettify OFF)", sample.name))
PakettiAutoSamplifyApplyLoaderSettings(sample)
end
end
renoise.app():show_status(string.format("Applied sample settings to %d samples in instrument %d (Pakettify OFF)", new_sample_count, instr_idx))
elseif is_pakettified then
-- Instrument already pakettified: Apply settings in place
print(string.format("Instrument %d already pakettified: Applying settings to %d samples in place", instr_idx, new_sample_count))
for _, sample_info in ipairs(samples) do
local sample = instrument.samples[sample_info.sample_index]
if sample then
-- Mark as processed before applying settings (name|frames key)
local _fc = (sample.sample_buffer and sample.sample_buffer.has_sample_data)
and sample.sample_buffer.number_of_frames or 0
track_file(sample.name .. "|" .. tostring(_fc))
dbg(string.format("DEBUG: Marking sample '%s' as processed (already pakettified)", sample.name))
PakettiAutoSamplifyApplyLoaderSettings(sample)
end
end
renoise.app():show_status(string.format("Applied Paketti settings to %d samples in existing pakettified instrument %d", new_sample_count, instr_idx))
elseif has_external_samples and new_sample_count > 1 then
-- Multiple external samples in one instrument: Create one new instrument for all samples
print(string.format("Multiple external samples (%d) in instrument %d: Creating single new instrument", new_sample_count, instr_idx))
-- Store source sample indices BEFORE creating new instrument
local source_indices = {}
for _, sample_info in ipairs(samples) do
local source_sample = instrument.samples[sample_info.sample_index]