forked from esaruoho/paketti
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPakettiOTExport.lua
More file actions
3853 lines (3221 loc) · 155 KB
/
PakettiOTExport.lua
File metadata and controls
3853 lines (3221 loc) · 155 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
-- Control variable for showing debug dialog after drag & drop import
-- Set to true to show debug dialog when dragging .ot files into Renoise
-- Set to false to import silently (default behavior)
local show_debug_dialog_on_import = true
local header = {
0x46, 0x4F, 0x52, 0x4D,
0x00, 0x00, 0x00, 0x00,
0x44, 0x50, 0x53, 0x31,
0x53, 0x4D, 0x50, 0x41 };
local unknown = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00 };
function wb32(f, x)
local b4 = string.char(x % 256) x = (x - x % 256) / 256
local b3 = string.char(x % 256) x = (x - x % 256) / 256
local b2 = string.char(x % 256) x = (x - x % 256) / 256
local b1 = string.char(x % 256) x = (x - x % 256) / 256
f:write(b1, b2, b3, b4) -- Big-endian: MSB first
end
function wb16(f, x)
local b2 = string.char(x % 256) x = (x - x % 256) / 256
local b1 = string.char(x % 256) x = (x - x % 256) / 256
f:write(b1, b2) -- Big-endian: MSB first
end
-- Function to write 16-bit value with byte order reversal for checksum (DEPRECATED - not used anymore)
function wb16_reversed(f, x)
local b2 = string.char(x % 256) x = (x - x % 256) / 256
local b1 = string.char(x % 256) x = (x - x % 256) / 256
f:write(b2, b1) -- Reversed byte order for little-endian checksum
end
function wb(f, x)
f:write(string.char(x))
end
function wb_table(f, data)
for k, v in ipairs(data) do
wb(f, v);
end
end
function w_slices(f, slices)
for k, slice in ipairs(slices) do
wb32(f, slice.start_point);
wb32(f, slice.end_point);
wb32(f, slice.loop_point);
end
end
-- Creates .ot file data table with correct Octatrack format specifications
-- Based on OctaChainer source code analysis for 100% compatibility
-- Key implementations matching OctaChainer exactly:
-- 1. Endianness: BIG-ENDIAN (MSB first) for all 16/32-bit values
-- 2. Tempo: BPM × 24 (matches OctaChainer tempo*6 where tempo=BPM*4)
-- 3. Trim/Loop Length: bars × 25, where bars = (BPM × frames) / (sampleRate × 60 × 4)
-- 4. Gain: User gain + 48 offset (48 = 0dB)
-- 5. Slice positions: First slice MUST start at 0, others convert 1-based→0-based
-- 6. Checksum: Sum bytes 16-829 (OctaChainer method), 16-bit wrap only
-- 7. File size: Exactly 832 bytes
-- 8. Unknown bytes: {0x00,0x00,0x00,0x00,0x00,0x02,0x00} (matches OctaChainer)
function make_ot_table(sample)
local sample_buffer = sample.sample_buffer
local slice_count = table.getn(sample.slice_markers)
local sample_len = sample_buffer.number_of_frames
-- These variables need to be accessible for debug prints
local sample_rate = sample.sample_buffer.sample_rate
local bpm = renoise.song().transport.bpm
-- Try to extract OT metadata from sample name first
local tempo_value, trim_loop_value, loop_len_value, stretch_value, loop_value, gain_value, stored_slice_ends, trim_end_value
print("PakettiOTExport: DEBUG - Sample name: '" .. sample.name .. "'")
print("PakettiOTExport: DEBUG - Current song BPM: " .. bpm)
-- Try newest format with TE (trim_end) field first
local t_val, tl_val, ll_val, s_val, l_val, g_val, te_val, e_val = sample.name:match("OT%[T(%d+):TL(%d+):LL(%d+):S(%d+):L(%d+):G(%d+):TE(%d+):E=([%d,]*)%]")
print("PakettiOTExport: DEBUG - Pattern match results (NEW with TE):")
print("PakettiOTExport: DEBUG - t_val=" .. tostring(t_val))
print("PakettiOTExport: DEBUG - tl_val=" .. tostring(tl_val))
print("PakettiOTExport: DEBUG - ll_val=" .. tostring(ll_val))
print("PakettiOTExport: DEBUG - s_val=" .. tostring(s_val))
print("PakettiOTExport: DEBUG - l_val=" .. tostring(l_val))
print("PakettiOTExport: DEBUG - g_val=" .. tostring(g_val))
print("PakettiOTExport: DEBUG - te_val=" .. tostring(te_val))
print("PakettiOTExport: DEBUG - e_val=" .. tostring(e_val))
-- Try format without TE field for backward compatibility
if not t_val then
print("PakettiOTExport: DEBUG - NEW with TE failed, trying format without TE")
t_val, tl_val, ll_val, s_val, l_val, g_val, e_val = sample.name:match("OT%[T(%d+):TL(%d+):LL(%d+):S(%d+):L(%d+):G(%d+):E=([%d,]*)%]")
te_val = nil -- No TE field in this format
if t_val then
print("PakettiOTExport: DEBUG - Found format without TE")
end
else
print("PakettiOTExport: DEBUG - Found NEW format with TE field")
end
-- Try old format without slice ends for backward compatibility
if not t_val then
print("PakettiOTExport: DEBUG - Trying OLD format without slice ends")
t_val, tl_val, ll_val, s_val, l_val, g_val = sample.name:match("OT%[T(%d+):TL(%d+):LL(%d+):S(%d+):L(%d+):G(%d+)%]")
te_val = nil
e_val = nil
if t_val then
print("PakettiOTExport: DEBUG - Found OLD format metadata")
print("PakettiOTExport: DEBUG - OLD t_val=" .. tostring(t_val))
end
end
if not t_val then
print("PakettiOTExport: DEBUG - NO metadata found in sample name, using fallback")
end
if t_val then
-- Use stored OT metadata from import
tempo_value = tonumber(t_val)
trim_loop_value = tonumber(tl_val)
loop_len_value = tonumber(ll_val)
stretch_value = tonumber(s_val)
loop_value = tonumber(l_val)
gain_value = tonumber(g_val)
-- Always use current sample length for trim_end (don't trust stored values)
trim_end_value = sample_len
print("PakettiOTExport: Using current sample length for trim_end: " .. trim_end_value)
-- Parse stored slice ends
stored_slice_ends = {}
if e_val and e_val ~= "" then
for end_pos in e_val:gmatch("(%d+)") do
table.insert(stored_slice_ends, tonumber(end_pos))
end
end
print("PakettiOTExport: SUCCESSFULLY USING STORED OT METADATA")
print(string.format("PakettiOTExport: Tempo=%d (BPM: %d), TrimLen=%d, LoopLen=%d, Stretch=%d, Loop=%d, Gain=%d",
tempo_value, math.floor(tempo_value/24), trim_loop_value, loop_len_value, stretch_value, loop_value, gain_value))
print(string.format("PakettiOTExport: trim_end=%d, sample_len=%d", trim_end_value, sample_len))
print(string.format("PakettiOTExport: Found %d stored slice ends", #stored_slice_ends))
-- Show first few slice ends for verification
if stored_slice_ends and #stored_slice_ends > 0 then
local preview = {}
for i = 1, math.min(5, #stored_slice_ends) do
table.insert(preview, tostring(stored_slice_ends[i]))
end
if #stored_slice_ends > 5 then
table.insert(preview, "...")
end
print(string.format("PakettiOTExport: Slice ends preview: %s", table.concat(preview, ", ")))
end
else
-- Fallback: compute values from current Renoise state
tempo_value = math.floor(bpm * 24)
-- Calculate trim_len and loop_len using OctaChainer's bar-based formula
-- bars = (BPM * totalSampleCount) / (sampleRate * 60.0 * 4) + 0.5 (rounded)
-- stored_value = bars * 25
local bars = math.floor(((bpm * sample_len) / (sample_rate * 60.0 * 4)) + 0.5)
trim_loop_value = bars * 25
loop_len_value = trim_loop_value -- Default loop length = full sample length
stretch_value = 0 -- Use stretch=0 (Off) like OctaChainer
loop_value = 0 -- Use loop=0 (Off) like OctaChainer
gain_value = 48 -- 0dB gain (48 = 0dB offset)
trim_end_value = sample_len -- Use actual sample length when no metadata available
stored_slice_ends = nil
print("PakettiOTExport: Computing new OT values using OctaChainer method")
print(string.format("PakettiOTExport: NEW SAMPLE - Tempo=%d (BPM: %d), TrimLen=%d, LoopLen=%d",
tempo_value, math.floor(tempo_value/24), trim_loop_value, loop_len_value))
print(string.format("PakettiOTExport: NEW SAMPLE - Sample: %d frames, %.1fkHz → bars=%.2f → trim_len=%d",
sample_len, sample_rate, bars, trim_loop_value))
end
-- Limit slice count to 64 (Octatrack maximum)
local export_slice_count = math.min(slice_count, 64)
-- Final values check before creating .ot table
print("=== FINAL VALUES FOR .OT EXPORT ===")
print(string.format("FINAL tempo_value: %d (BPM: %d)", tempo_value, math.floor(tempo_value/24)))
print(string.format("FINAL trim_loop_value: %d", trim_loop_value))
print(string.format("FINAL trim_end_value: %d", trim_end_value))
print("=====================================")
-- Debug prints
print("sample length: " .. sample_len .. " frames")
print("sample rate: " .. sample_rate .. " Hz")
print("tempo: " .. bpm .. " BPM (stored as " .. tempo_value .. ")")
print("trim/loop length: " .. trim_loop_value .. " (OctaChainer bars × 25 method)")
print("total slices: " .. slice_count .. ", exporting: " .. export_slice_count)
-- Warn if there are more slices than the Octatrack can handle
if slice_count > 64 then
print("WARNING: Sample has " .. slice_count .. " slices, but .ot format only supports 64 slices maximum.")
print("Only the first 64 slices will be exported to the .ot file.")
renoise.app():show_status("Exporting first 64 of " .. slice_count .. " slices (.ot format limit)")
elseif slice_count > 0 then
print("Exporting all " .. slice_count .. " slices to .ot file.")
renoise.app():show_status("Exporting " .. slice_count .. " slices to .ot file")
else
print("No slices found in sample - exporting .ot file without slice data.")
renoise.app():show_status("No slices found - exporting .ot file without slice data")
end
local ot = {}
-- Insert header and unknown
for k, v in ipairs(header) do
table.insert(ot, v)
end
for k, v in ipairs(unknown) do
table.insert(ot, v)
end
-- tempo (32)
table.insert(ot, tempo_value)
-- trim_len (32) (frames × 100 / sampleRate per OctaChainer spec)
table.insert(ot, trim_loop_value)
-- loop_len (32) (frames × 100 / sampleRate per OctaChainer spec)
table.insert(ot, loop_len_value)
-- stretch (32)
table.insert(ot, stretch_value)
-- loop (32) (0 = off)
table.insert(ot, loop_value)
-- gain (16) (user_gain + 48, where 0 dB = 48)
table.insert(ot, gain_value)
-- quantize (8)
table.insert(ot, 0xFF)
-- trim_start (32)
table.insert(ot, 0x00)
-- trim_end (32)
table.insert(ot, trim_end_value)
-- loop_point (32)
table.insert(ot, 0x00)
-- Process only the first 64 slices (or fewer if less than 64 exist)
for k = 1, export_slice_count do
local v = sample.slice_markers[k]
local nxt = (k < export_slice_count) and sample.slice_markers[k + 1] or sample_len
print("slice " .. k .. ": " .. v .. ", next: " .. nxt)
-- Convert from 1-based (Renoise) to 0-based (Octatrack) indexing
-- CRITICAL: First slice must start at frame 0 for Octatrack
local s_start = (k == 1) and 0 or (v - 1) -- Octatrack demands start = 0
-- Always calculate slice end from current sample, don't use stored positions
-- (stored positions might be from different sample rate/length)
local s_end
if k < export_slice_count then
s_end = sample.slice_markers[k + 1] - 2 -- next start - 1, converted to 0-based
else
s_end = sample_len - 1 -- last slice ends at sample end - 1
end
-- Ensure slice end is within sample bounds
s_end = math.max(s_start, math.min(s_end, sample_len - 1))
print("slice " .. k .. ": calculated end position " .. s_end .. " (bounds-checked)")
-- start_point (32)
table.insert(ot, s_start)
-- end_point (32)
table.insert(ot, s_end)
-- loop_point (32)
table.insert(ot, 0xFFFFFFFF)
print("slice " .. k .. ": start=" .. s_start .. ", end=" .. s_end)
end
-- No empty slice filling - just write actual slices and pad the rest
-- slice_count (32)
table.insert(ot, export_slice_count)
-- Checksum will be calculated and appended by write_ot_file on the actual byte stream
print("OT table created, checksum will be calculated on byte stream")
-- DEBUG: Show the ot table structure
print("=== DEBUG: OT TABLE STRUCTURE ===")
print("Total ot table size:", #ot)
print("ot[24] (should be tempo):", ot[24])
print("ot[25] (should be trim_len):", ot[25])
print("ot[32] (should be trim_end):", ot[32])
print("=================================")
return ot
end
function write_ot_file(filename, ot)
local ot_filename
-- Check if filename already ends with .ot
if filename:match("%.ot$") then
ot_filename = filename
else
-- Extract base name without extension
local name = filename:match("(.+)%..+$")
-- Fallback if pattern match fails or filename has no extension
if not name then
name = filename
end
ot_filename = name .. ".ot"
end
-- Build complete byte array first (832 bytes exactly)
local byte_array = {}
-- Helper function to append bytes from integer (big-endian)
local function append_be32(value)
local b4 = value % 256; value = math.floor(value / 256)
local b3 = value % 256; value = math.floor(value / 256)
local b2 = value % 256; value = math.floor(value / 256)
local b1 = value % 256
table.insert(byte_array, b1) -- MSB first
table.insert(byte_array, b2)
table.insert(byte_array, b3)
table.insert(byte_array, b4) -- LSB last
end
local function append_be16(value)
local b2 = value % 256; value = math.floor(value / 256)
local b1 = value % 256
table.insert(byte_array, b1) -- MSB first
table.insert(byte_array, b2) -- LSB last
end
local function append_byte(value)
table.insert(byte_array, value)
end
-- Write header and unknown (bytes 1-23, single bytes)
for i = 1, 23 do
append_byte(ot[i])
end
-- Write main data section (starting at byte 24, offset 0x17)
local data_start = 24 -- Start of checksummed region
-- DEBUG: Show what values are actually being written
print("=== DEBUG: ACTUAL VALUES BEING WRITTEN TO FILE ===")
print("ot[24] (tempo):", ot[24], "(BPM:", math.floor(ot[24]/24) .. ")")
print("ot[25] (trim_len):", ot[25])
print("ot[26] (loop_len):", ot[26])
print("ot[27] (stretch):", ot[27])
print("ot[28] (loop):", ot[28])
print("ot[29] (gain):", ot[29])
print("ot[30] (quantize):", ot[30])
print("ot[31] (trim_start):", ot[31])
print("ot[32] (trim_end):", ot[32])
print("ot[33] (loop_point):", ot[33])
print("====================================================")
append_be32(ot[24]) -- tempo
append_be32(ot[25]) -- trim_len
append_be32(ot[26]) -- loop_len
append_be32(ot[27]) -- stretch
append_be32(ot[28]) -- loop
append_be16(ot[29]) -- gain
append_byte(ot[30]) -- quantize
append_be32(ot[31]) -- trim_start
append_be32(ot[32]) -- trim_end
append_be32(ot[33]) -- loop_point
-- Write actual slice data (variable number of slices)
local slice_data_start = 34 -- First slice data in ot table
local actual_slice_count = ot[#ot] -- Last element is slice_count
local slice_fields_written = 0
print("=== SLICE DATA WRITING ===")
print("Writing " .. actual_slice_count .. " slices")
-- Write actual slices
for i = slice_data_start, #ot - 1 do -- -1 to exclude slice_count
append_be32(ot[i])
slice_fields_written = slice_fields_written + 1
if (slice_fields_written % 3) == 0 then
local slice_num = slice_fields_written / 3
print("Wrote slice " .. slice_num .. " data")
end
end
-- Pad remaining slice slots with zeros (up to 64 slices total)
local max_slice_fields = 64 * 3 -- 64 slices × 3 fields each
for i = slice_fields_written + 1, max_slice_fields do
append_be32(0)
end
print("Total slice fields written: " .. slice_fields_written)
print("Zero-padded fields: " .. (max_slice_fields - slice_fields_written))
print("=== END SLICE DATA ===")
-- Write slice_count
append_be32(actual_slice_count)
-- Calculate checksum using OctaChainer method: sum bytes 16 to 829 (no adjustments)
local checksum = 0
local checksum_bytes = {}
print("=== CHECKSUM CALCULATION DEBUG (OctaChainer method) ===")
for i = 17, 830 do -- Convert to 1-based indexing: C++ bytes 16-829 = Lua indices 17-830
if byte_array[i] then
checksum = checksum + byte_array[i]
table.insert(checksum_bytes, byte_array[i])
if i <= 25 or i >= 826 then -- Show first few and last few bytes
print(string.format("byte[%d] = 0x%02X (%d), running sum = %d", i-1, byte_array[i], byte_array[i], checksum))
elseif i == 26 then
print("... (omitting middle bytes for readability) ...")
end
if checksum > 0xFFFF then
checksum = checksum % 0x10000 -- 16-bit wrap
end
end
end
print(string.format("OctaChainer checksum sum: %d (0x%04X)", checksum, checksum))
print(string.format("Checksum range: C++ bytes 16 to 829 (%d bytes total)", #checksum_bytes))
print("=== END CHECKSUM DEBUG ===")
-- Show the actual checksum bytes that will be written
local checksum_hi = math.floor(checksum / 256)
local checksum_lo = checksum % 256
print(string.format("Final checksum bytes: 0x%02X 0x%02X (big-endian %d)", checksum_hi, checksum_lo, checksum))
-- Append checksum (16-bit big-endian)
append_be16(checksum)
-- Ensure exactly 832 bytes
while #byte_array < 832 do
append_byte(0)
end
-- Show complete hexdump of what we're writing (832 bytes)
hexdump(byte_array, 0, 832, "COMPLETE EXPORTED .OT FILE (832 bytes)")
-- Write to file
local f = io.open(ot_filename, "wb")
for i = 1, 832 do
f:write(string.char(byte_array[i] or 0))
end
f:close()
print("PakettiOTExport: .ot file written: " .. ot_filename)
print("PakettiOTExport: .ot file size: 832 bytes (exactly as per OctaChainer spec)")
print("PakettiOTExport: checksum calculated: " .. checksum)
end
-- Hexdump function for debugging
function hexdump(data, start_offset, length, label)
print("=== HEXDUMP: " .. label .. " ===")
local end_offset = math.min(start_offset + length - 1, #data - 1)
for i = start_offset, end_offset, 16 do
local hex_part = ""
local ascii_part = ""
local line_start = string.format("%08X: ", i)
for j = 0, 15 do
if i + j <= end_offset then
local byte_val = type(data) == "string" and string.byte(data, i + j + 1) or data[i + j + 1]
if byte_val then
hex_part = hex_part .. string.format("%02X ", byte_val)
ascii_part = ascii_part .. (byte_val >= 32 and byte_val <= 126 and string.char(byte_val) or ".")
else
hex_part = hex_part .. " "
ascii_part = ascii_part .. " "
end
else
hex_part = hex_part .. " "
ascii_part = ascii_part .. " "
end
end
print(line_start .. hex_part .. " |" .. ascii_part .. "|")
end
print("=== END HEXDUMP ===")
end
-- Binary reading functions for .ot import (BIG-ENDIAN format)
function rb32(f)
local b1 = string.byte(f:read(1) or "\0") -- MSB first
local b2 = string.byte(f:read(1) or "\0")
local b3 = string.byte(f:read(1) or "\0")
local b4 = string.byte(f:read(1) or "\0") -- LSB last
return b1 * 256^3 + b2 * 256^2 + b3 * 256 + b4 -- BIG-ENDIAN
end
function rb16(f)
local b1 = string.byte(f:read(1) or "\0") -- MSB first
local b2 = string.byte(f:read(1) or "\0") -- LSB last
return b1 * 256 + b2 -- BIG-ENDIAN
end
function rb(f)
return string.byte(f:read(1) or "\0")
end
function rb_table(f, count)
local data = {}
for i = 1, count do
table.insert(data, rb(f))
end
return data
end
-- Function to read and parse .ot file
function read_ot_file(filename)
local f = io.open(filename, "rb")
if not f then
renoise.app():show_status("Could not open .ot file: " .. filename)
print("PakettiOTImport: Could not open .ot file: " .. filename)
return nil
end
print("PakettiOTImport: Reading .ot file: " .. filename)
-- Read entire file for hexdump analysis
f:seek("set", 0) -- Reset to beginning
local file_data = f:read("*all")
f:close()
-- Show complete hexdump of entire .ot file (832 bytes)
hexdump(file_data, 0, 832, "COMPLETE IMPORTED .OT FILE (832 bytes)")
-- Reopen file for normal parsing
f = io.open(filename, "rb")
if not f then
print("PakettiOTImport: Could not reopen .ot file")
return nil
end
-- Read header (16 bytes)
local header_data = rb_table(f, 16)
print("PakettiOTImport: Header read")
-- Read unknown section (7 bytes)
local unknown_data = rb_table(f, 7)
print("PakettiOTImport: Unknown section read")
-- Read main parameters
local tempo = rb32(f)
local trim_len = rb32(f)
local loop_len = rb32(f)
local stretch = rb32(f)
local loop = rb32(f)
local gain = rb16(f)
local quantize = rb(f)
local trim_start = rb32(f)
local trim_end = rb32(f)
local loop_point = rb32(f)
print("PakettiOTImport: Main parameters - trim_len: " .. trim_len .. ", loop_len: " .. loop_len)
-- Read slice data (64 slices max, 3 x 32-bit values each)
local slices = {}
for i = 1, 64 do
local start_point = rb32(f)
local end_point = rb32(f) -- This is end_point, matching C struct
local slice_loop_point = rb32(f)
-- Only add slices that have actual data (not all zeros)
if start_point > 0 or end_point > 0 then
table.insert(slices, {
start_point = start_point,
end_point = end_point, -- Store as end_point to match C struct
loop_point = slice_loop_point
})
print("PakettiOTImport: Slice " .. i .. " - start: " .. start_point .. ", end: " .. end_point)
end
end
-- Read slice count and checksum
local slice_count = rb32(f)
local checksum = rb16(f)
f:close()
-- Show imported checksum details
print("=== IMPORTED CHECKSUM ANALYSIS ===")
print(string.format("File checksum: %d (0x%04X)", checksum, checksum))
local checksum_hi = math.floor(checksum / 256)
local checksum_lo = checksum % 256
print(string.format("Checksum bytes: 0x%02X 0x%02X (big-endian)", checksum_hi, checksum_lo))
print("=== END IMPORTED CHECKSUM ===")
print("PakettiOTImport: Found " .. slice_count .. " slices in .ot file")
return {
tempo = tempo,
trim_len = trim_len,
loop_len = loop_len,
stretch = stretch,
loop = loop,
gain = gain,
quantize = quantize,
trim_start = trim_start,
trim_end = trim_end,
loop_point = loop_point,
slices = slices,
slice_count = slice_count,
checksum = checksum
}
end
-- Function to apply .ot slice data to a specific sample (direct)
function apply_ot_slices_to_sample_direct(ot_data, target_sample)
if not target_sample or not target_sample.sample_buffer.has_sample_data then
renoise.app():show_status("No valid sample provided to apply slices to")
print("PakettiOTImport: No valid sample provided")
return
end
local sample_length = target_sample.sample_buffer.number_of_frames
print("PakettiOTImport: Sample length is " .. sample_length .. " frames")
print("PakettiOTImport: Processing " .. #ot_data.slices .. " slices from .ot file")
-- Clear existing slice markers (proper way: delete all existing markers first)
local existing_markers = {}
for _, marker in ipairs(target_sample.slice_markers) do
table.insert(existing_markers, marker)
end
for _, marker in ipairs(existing_markers) do
target_sample:delete_slice_marker(marker)
end
print("PakettiOTImport: Cleared " .. #existing_markers .. " existing slice markers")
-- Apply slices from .ot data using proper API
local applied_slices = 0
local skipped_slices = 0
for i, slice in ipairs(ot_data.slices) do
-- Allow slice.start_point >= 0 and ensure it's within sample bounds
if slice.start_point >= 0 and slice.start_point < sample_length then
-- Convert from 0-based (Octatrack .ot) to 1-based (Renoise) indexing
local slice_position = slice.start_point + 1
-- Use proper API method to insert slice marker
local success, error_msg = pcall(function()
target_sample:insert_slice_marker(slice_position)
end)
if success then
applied_slices = applied_slices + 1
print("PakettiOTImport: Applied slice " .. applied_slices .. " at position " .. slice_position .. " (from start_point " .. slice.start_point .. ")")
else
print("PakettiOTImport: Error inserting slice marker at position " .. slice_position .. ": " .. tostring(error_msg))
skipped_slices = skipped_slices + 1
end
else
print("PakettiOTImport: Skipping slice " .. i .. " - start_point " .. slice.start_point .. " is out of bounds (sample length: " .. sample_length .. ")")
skipped_slices = skipped_slices + 1
end
end
if applied_slices > 0 then
renoise.app():show_status("Applied " .. applied_slices .. " slices from .ot file")
print("PakettiOTImport: Successfully applied " .. applied_slices .. " slices to sample")
else
renoise.app():show_status("No slices applied - all slice positions were out of bounds or at position 0")
print("PakettiOTImport: No slices applied - " .. skipped_slices .. " slices were skipped")
end
if skipped_slices > 0 then
print("PakettiOTImport: Total skipped slices: " .. skipped_slices)
end
end
-- Function to apply .ot slice data to current sample
function apply_ot_slices_to_sample(ot_data)
local song = renoise.song()
local sample = song.selected_sample
if not sample or not sample.sample_buffer.has_sample_data then
renoise.app():show_status("No valid sample selected to apply slices to")
print("PakettiOTImport: No valid sample selected")
return
end
local sample_length = sample.sample_buffer.number_of_frames
local instrument = song.selected_instrument
local sample_index = song.selected_sample_index
print("PakettiOTImport: Working with instrument '" .. instrument.name .. "' (index " .. song.selected_instrument_index .. "), sample '" .. sample.name .. "' (index " .. sample_index .. ")")
print("PakettiOTImport: Sample length is " .. sample_length .. " frames")
print("PakettiOTImport: Processing " .. #ot_data.slices .. " slices from .ot file")
-- Clear existing slice markers (proper way: delete all existing markers first)
local existing_markers = {}
for _, marker in ipairs(sample.slice_markers) do
table.insert(existing_markers, marker)
end
for _, marker in ipairs(existing_markers) do
sample:delete_slice_marker(marker)
end
print("PakettiOTImport: Cleared " .. #existing_markers .. " existing slice markers")
-- Apply slices from .ot data using proper API
local applied_slices = 0
local skipped_slices = 0
for i, slice in ipairs(ot_data.slices) do
-- Allow slice.start_point >= 0 and ensure it's within sample bounds
if slice.start_point >= 0 and slice.start_point < sample_length then
-- Convert from 0-based (Octatrack .ot) to 1-based (Renoise) indexing
local slice_position = slice.start_point + 1
-- Use proper API method to insert slice marker
local success, error_msg = pcall(function()
sample:insert_slice_marker(slice_position)
end)
if success then
applied_slices = applied_slices + 1
print("PakettiOTImport: Applied slice " .. applied_slices .. " at position " .. slice_position .. " (from start_point " .. slice.start_point .. ")")
else
print("PakettiOTImport: Error inserting slice marker at position " .. slice_position .. ": " .. tostring(error_msg))
skipped_slices = skipped_slices + 1
end
else
print("PakettiOTImport: Skipping slice " .. i .. " - start_point " .. slice.start_point .. " is out of bounds (sample length: " .. sample_length .. ")")
skipped_slices = skipped_slices + 1
end
end
if applied_slices > 0 then
renoise.app():show_status("Applied " .. applied_slices .. " slices from .ot file")
print("PakettiOTImport: Successfully applied " .. applied_slices .. " slices to sample")
else
renoise.app():show_status("No slices applied - all slice positions were out of bounds or at position 0")
print("PakettiOTImport: No slices applied - " .. skipped_slices .. " slices were skipped")
end
if skipped_slices > 0 then
print("PakettiOTImport: Total skipped slices: " .. skipped_slices)
end
end
-- Function to export only .ot file (no audio)
function PakettiOTExportOtOnly()
-- Check if there's a song
if not renoise.song() then
renoise.app():show_status("No song loaded")
print("PakettiOTExportOtOnly: No song loaded")
return
end
-- Check if there are any instruments
if not renoise.song().instruments or #renoise.song().instruments == 0 then
renoise.app():show_status("No instruments in song")
print("PakettiOTExportOtOnly: No instruments in song")
return
end
-- Check if there's a selected instrument
if not renoise.song().selected_instrument then
renoise.app():show_status("No instrument selected")
print("PakettiOTExportOtOnly: No instrument selected")
return
end
-- Check if the selected instrument has samples
if not renoise.song().selected_instrument.samples or #renoise.song().selected_instrument.samples == 0 then
renoise.app():show_status("Selected instrument has no samples")
print("PakettiOTExportOtOnly: Selected instrument has no samples")
return
end
-- Check if there's a selected sample
local sample = renoise.song().selected_sample
if not sample then
renoise.app():show_status("No sample selected")
print("PakettiOTExportOtOnly: No sample selected")
return
end
-- Check if the sample has a sample buffer
if not sample.sample_buffer then
renoise.app():show_status("Selected sample has no sample buffer")
print("PakettiOTExportOtOnly: Selected sample has no sample buffer")
return
end
-- Check if the sample buffer has frames
if not sample.sample_buffer.number_of_frames or sample.sample_buffer.number_of_frames <= 0 then
renoise.app():show_status("Selected sample has no audio data")
print("PakettiOTExportOtOnly: Selected sample has no audio data")
return
end
-- Check if slice_markers exists (initialize empty table if nil)
if not sample.slice_markers then
sample.slice_markers = {}
print("PakettiOTExportOtOnly: No slice markers found, using empty table")
end
-- Check if sample has a name (provide default if needed)
if not sample.name or sample.name == "" then
sample.name = "Unknown Sample"
print("PakettiOTExportOtOnly: Sample has no name, using default")
end
-- Check slice count and warn if over 64 (Octatrack limit)
local slice_count = sample.slice_markers and #sample.slice_markers or 0
if slice_count > 64 then
local result = renoise.app():show_prompt("Slice Limit Warning",
"Sample has " .. slice_count .. " slices, but Octatrack only supports 64.\n" ..
"Only the first 64 slices will be exported.\n\nContinue?",
{"Continue", "Cancel"})
if result == "Cancel" then
renoise.app():show_status("Export cancelled")
print("PakettiOTExportOtOnly: Export cancelled due to slice count")
return
end
print("PakettiOTExportOtOnly: Warning - Exporting only first 64 of " .. slice_count .. " slices")
end
print("PakettiOTExportOtOnly: All safety checks passed, proceeding with .ot export")
-- Refresh sample reference in case it was changed by any preprocessing
sample = renoise.song().selected_sample
local ot = make_ot_table(sample)
local filename = renoise.app():prompt_for_filename_to_write("*.ot", "Save .ot file...")
-- Check if user cancelled the file dialog
if not filename or filename == "" then
renoise.app():show_status("Export cancelled")
print("PakettiOTExportOtOnly: Export cancelled by user")
return
end
write_ot_file(filename, ot)
renoise.app():show_status(".ot file exported successfully")
print("PakettiOTExportOtOnly: .ot file export completed successfully")
end
-- Function to import .ot file and apply slices
function PakettiOTImport()
-- Check if there's a selected sample to apply slices to
if not renoise.song() then
renoise.app():show_status("No song loaded")
print("PakettiOTImport: No song loaded")
return
end
if not renoise.song().selected_sample or not renoise.song().selected_sample.sample_buffer.has_sample_data then
renoise.app():show_status("Please select a sample to apply .ot slices to")
print("PakettiOTImport: No valid sample selected")
return
end
local filename = renoise.app():prompt_for_filename_to_read({"*.ot"}, "Load .ot file...")
-- Check if user cancelled the file dialog
if not filename or filename == "" then
renoise.app():show_status("Import cancelled")
print("PakettiOTImport: Import cancelled by user")
return
end
local ot_data = read_ot_file(filename)
if ot_data then
apply_ot_slices_to_sample(ot_data)
renoise.app().window.active_middle_frame = renoise.ApplicationWindow.MIDDLE_FRAME_INSTRUMENT_SAMPLE_EDITOR
end
end
-- Reusable function to show .ot file debug information in a dialog
function show_ot_debug_dialog(ot_data, filename, extra_info, show_apply_button, apply_callback)
-- Convert tempo back to BPM (tempo_value / 24 per Octatrack spec)
local calculated_bpm = math.floor(ot_data.tempo / 24)
-- Build debug info string
local debug_info = string.format([[
File: %s%s
MAIN PARAMETERS:
- Tempo: %d (BPM: %d)
- Trim Length: %d (frames×100/sampleRate)
- Loop Length: %d (frames×100/sampleRate)
- Stretch: %d (0=Off, 2=Normal, 3=Beat)
- Loop: %d (0=Off, 1=Normal, 2=PingPong)
- Gain: %d (dB offset: %+d, where 48=0dB)
- Quantize: 0x%02X (0xFF=Direct, 0x00=Pattern)
- Trim Start: %d
- Trim End: %d
- Loop Point: %d
- Slice Count: %d
- Checksum: %d
SLICES (%d found):]],
filename:match("([^/\\]+)$") or filename,
extra_info or "",
ot_data.tempo, calculated_bpm,
ot_data.trim_len, ot_data.loop_len,
ot_data.stretch, ot_data.loop,
ot_data.gain, ot_data.gain - 48, ot_data.quantize,
ot_data.trim_start, ot_data.trim_end, ot_data.loop_point,
ot_data.slice_count, ot_data.checksum,
#ot_data.slices)
-- Add ALL slice information (no truncation)
for i, slice in ipairs(ot_data.slices) do
debug_info = debug_info .. string.format("\n%2d: Start=%d, End=%d, Loop=0x%08X",
i, slice.start_point, slice.end_point, slice.loop_point)
end
-- Show in a custom dialog
local vb = renoise.ViewBuilder()
local debug_dialog = nil -- Store dialog handle for proper closing
-- Build button row
local button_row = vb:horizontal_aligner {
mode = show_apply_button and "distribute" or "right"
}
if show_apply_button and apply_callback then
button_row:add_child(vb:button {
text = "Apply Slices to Current Sample",
width = 200,
released = apply_callback
})
end
button_row:add_child(vb:button {
text = "Close",
width = 100,
released = function()
if debug_dialog and debug_dialog.visible then
debug_dialog:close()
end
end
})
local content = vb:column {
margin = 10,
vb:multiline_textfield {
text = debug_info,
width = 600,
height = 700,
font = "mono"
},
button_row
}
debug_dialog = renoise.app():show_custom_dialog("Octatrack .OT File Analysis", content)
end
-- Function to show .ot file debug information in a dialog
function PakettiOTDebugDialog()
local filename = renoise.app():prompt_for_filename_to_read({"*.ot"}, "Load .ot file for analysis...")
if not filename or filename == "" then
return -- User cancelled
end
local ot_data = read_ot_file(filename)
if not ot_data then
renoise.app():show_error("OT Debug Error", "Could not read .ot file: " .. filename)
return
end
show_ot_debug_dialog(ot_data, filename)
end
-- File import hook for .ot files (drag & drop support)
function ot_import_filehook(filename)
if not filename then
renoise.app():show_error("OT Import Error: No filename provided!")
return false
end
print("Starting OT import via file hook for file:", filename)
-- Extract base filename for naming
local ot_basename = filename:match("([^/\\]+)$"):gsub("%.ot$", "")
-- First, read and analyze the .ot file
local ot_data = read_ot_file(filename)
if not ot_data then
renoise.app():show_error("OT Import Error", "Could not read .ot file: " .. filename)
return false
end
-- Look for corresponding .wav file in the same directory
local base_path = filename:match("(.+)%..+$") -- Remove .ot extension
local wav_filename = base_path .. ".wav"
local wav_found = false
-- Check if .wav file exists
local function file_exists(name)
local f = io.open(name, "rb")
if f then f:close() end
return f ~= nil
end
wav_found = file_exists(wav_filename)