-
Notifications
You must be signed in to change notification settings - Fork 1
/
play_s3m.lua
1818 lines (1636 loc) · 52.1 KB
/
play_s3m.lua
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
-- Scream Tracker 3 "S3M" playroutine
-- by zorg @ 2017 § ISC
-- TODO: Scream Tracker mentions a Xxx command that mapped to an amiga 8xx command, that's not used...
-- OpenMPT S3M Test battery:
----------------------------
-- Amiga Limits - Fail
-- Freq Limits - Pass
-- Loop Reset - Pass
-- NOP - Fail
-- OxxMem - Fail
-- ParamMem - Fail
-- PatternDelay - Fail?
-- PeriodLimit - Fail
-- PortaAfterArp - Fail
-- PortaSmpChange - Fail
-- VibratoTypeCh - Pass
-- Weirdloop - Fail, spectacularly at that.
-- Note: To keep things compact, everything not generic enough to be used by
-- other playroutines are kept inside the respective play_*.lua files,
-- a.k.a. these ones.
local Device = require 'device'
local device = Device(44100, 16, 2, 1024, 'Buffer', 'Buffer')
-- Start defining everything as local, then if we need something to be passed
-- into something that's a bit more "closed", redefine it as a var. of routine.
local source = device.source
local buffer = device.buffer
local module, voice, visualizer
local tickPeriod, samplingPeriod, midiPPQ, actualTempo
local timeSigNumer, timeSigDenom
--
local normalizer, normRatio, samplesToMix
local interpolation
local tickAccumulator, currentTick, currentRow, currentOrder, currentPattern
local time
local smoothScrolling
local speed, tempo
local loopRow, loopCnt, patternLoop, filterSet
local positionJump, patternBreak, patternDelay, globalVolume
local patternInvalidated = true
-- Constants
-- TODO: do dim. analysis on these numbers so we can reason about them better.
local ARPEGGIOPERIOD = 1 / 50 -- Hz; ST3's arp isn't tied to the speed var.
local SINETABLE = {
[0] = 0, 24, 49, 74, 97, 120, 141, 161,
180, 197, 212, 224, 235, 244, 250, 253,
255, 253, 250, 244, 235, 224, 212, 197,
180, 161, 141, 120, 97, 74, 49, 24,
0, 24, 49, 74, 97, 120, 141, 161,
180, 197, 212, 224, 235, 244, 250, 253,
255, 253, 250, 244, 235, 224, 212, 197,
180, 161, 141, 120, 97, 74, 49, 24}
local RAMPDOWNTABLE = {
[0] = 255, 247, 239, 231, 223, 215, 207, 199,
191, 183, 175, 167, 159, 151, 143, 135,
127, 119, 111, 103, 95, 87, 79, 71,
63, 55, 47, 39, 31, 23, 15, 7,
0, 8, 16, 24, 32, 40, 48, 56,
64, 72, 80, 88, 96, 104, 112, 120,
128, 136, 144, 152, 160, 168, 176, 184,
192, 200, 208, 216, 224, 232, 240, 248}
local SQUARETABLE = {
[0] = 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255}
-- This might not be what scream tracker did, but what it did, did sound random.
-- Until exact code is shown, this is "close enough".
local RANDOMTABLE = {}
for i = 0, 63 do RANDOMTABLE[i] = love.math.random(0, 255) end
local WAVEFORMTABLE = {
[0] = SINETABLE, RAMPDOWNTABLE, SQUARETABLE, RANDOMTABLE,
SINETABLE, RAMPDOWNTABLE, SQUARETABLE, RANDOMTABLE}
-- TODO: Test how scream tracker implemented the 8th type (?).
local RETRIGVOLSLIDEFUNC = {
[0] = function(v) return v end,
--[[1]] function(v) return v - 1 end,
--[[2]] function(v) return v - 2 end,
--[[3]] function(v) return v - 4 end,
--[[4]] function(v) return v - 8 end,
--[[5]] function(v) return v - 16 end,
--[[6]] function(v) return v * (2/3) end,
--[[7]] function(v) return v * (1/2) end,
--[[8]] function(v) return v end,
--[[9]] function(v) return v + 1 end,
--[[A]] function(v) return v + 2 end,
--[[B]] function(v) return v + 4 end,
--[[C]] function(v) return v + 8 end,
--[[D]] function(v) return v + 16 end,
--[[E]] function(v) return v * (3/2) end,
--[[F]] function(v) return v * (2/1) end,
}
local C4SPEEDFINETUNES = {
[ 0x00 ] = 7895, -- -8
[ 0x01 ] = 7941,
[ 0x02 ] = 7985,
[ 0x03 ] = 8046,
[ 0x04 ] = 8107,
[ 0x05 ] = 8169,
[ 0x06 ] = 8232,
[ 0x07 ] = 8280,
[ 0x08 ] = 8363, -- Default
[ 0x09 ] = 8413,
[ 0x0A ] = 8463,
[ 0x0B ] = 8529,
[ 0x0C ] = 8581,
[ 0x0D ] = 8651,
[ 0x0E ] = 8723,
[ 0x0F ] = 8757, -- +7
}
local DEFAULTC4SPEED = C4SPEEDFINETUNES[ 0x08 ]
-- Hz = DEFAULTC4SPEED * C4PERIOD / NOTEPERIOD
local OCTAVE4PERIOD = {
1712, 1616, 1524, 1440, 1356, 1280, 1208, 1140, 1076, 1016, 960, 907
}
local NOTEPERIOD = {}
for octave = 0, 10 do for note = 1, 12 do
NOTEPERIOD[octave*12+(note-1)] =
math.floor(16 * (OCTAVE4PERIOD[note] / 2^octave))
end end
local BASECLOCK = DEFAULTC4SPEED * OCTAVE4PERIOD[1] -- C4
local FIXEDCLOCK = BASECLOCK / device.samplingRate
local FIXTIMING = function(tempo, speed, tsn, tsd)
local tick = 2.5 / tempo
local ppq = speed * tsn * (4.0 / tsd)
local tempo = 60.0 / (tick * ppq)
return tick, ppq, tempo
end
local PERIODBINSEARCH = function(p)
-- Start from the middle and always halve the remaining area
-- We can assume that the given parameter will have a value between
-- 27392 and 0 (or it'll just return those as the "closest" ones.)
-- Note that the order of values is reversed (from highest to lowest)
local L, R = 0, 132-1
local m
while L <= R do
m = math.floor((L + R) / 2.0)
if NOTEPERIOD[m] > p then
L = m + 1
elseif NOTEPERIOD[m] < p then
R = m - 1
else
return m, 0
end
end
if not NOTEPERIOD[L-1] then return 0, 0 end
if not NOTEPERIOD[L] then return L-1, 0 end
return L-1, (p-NOTEPERIOD[L-1])/(NOTEPERIOD[L]-NOTEPERIOD[L-1])
end
-- Voice objects
local Voice = {}
Voice.getStatistics = function(v)
local smpL, smpS, smpE, Cspd, T, L, H, S = 0, 0, 0, 0, 0, 0, 0, 0
if v.instrument then
smpL = v.instrument.length or 0
smpS = v.instrument.loopStart or 0
smpE = v.instrument.loopEnd or 0
Cspd = v.instrument.c4speed or 0
T = v.instrument.type
L = v.instrument.looped and 0 or 1
H = v.instrument.bitDepth == 16 and 1 or 0
S = v.instrument.channelCount == 2 and 1 or 0
end
return v.n or 0xFF, v.i or 0, v.v or 0, v.c or 0, v.d or 0,
v.notePeriod, v.glisPeriod, v.instPeriod,
v.currOffset, smpL, smpS, smpE, Cspd, T, L, H, S,
v.currInstrument, v.currVolume*0x40, v.currPanning*0xF, v.fxCommand,
v.fxSlotGeneric, v.fxSlotPortamento, v.fxSlotVibrato,
loopRow[v.ch], loopCnt[v.ch],
v.noteDelayTicks, v.noteCutTicks,
v.arpIndex,
v.tremorOffset, v.tremorOnTicks, v.tremorOffTicks,
v.vibratoWaveform, v.vibratoOffset,
v.tremoloWaveform, v.tremoloOffset
end
Voice.setNote = function(v, note)
v.n = note
end
Voice.setInstrument = function(v, instrument)
v.i = instrument
end
Voice.setVolume = function(v, volume)
v.v = volume
end
Voice.setEffect = function(v, effectCommand, effectData)
v.c, v.d = effectCommand, effectData
end
Voice.setPeriod = function(v, pitch)
-- Voice.process calls this; set raw note period, and the fixed value
-- modified by the instrument c4speed.
if pitch == -1 then
v.notePeriod = 0
v.instPeriod = 0
return
end
v.notePeriod = NOTEPERIOD[pitch]
-- If amiga limits are forced, apply upper limits on base period as well,
-- not just the computed one.
if module.amigaNoteLimits then
v.notePeriod = math.max(v.notePeriod, 56) -- B-5
end
if v.instrument and v.instrument.c4speed then
v.instPeriod = v.notePeriod * (DEFAULTC4SPEED / v.instrument.c4speed)
-- Also enforce amiga limits on the computed period.
if module.amigaNoteLimits then
v.instPeriod = math.max(v.instPeriod, 56) -- B-5
end
v.tempPeriod = v.instPeriod -- Needed for semitone glissando.
end
end
Voice.process = function(v, currentTick)
local N, I, V, C, D
local Dx, Dy
-- Handle inputs
if v.n then
if v.n == 255 then
-- Note continue
N = false
elseif v.n == 254 then
-- Note cut
v.notePeriod = 0
N = -1
elseif v.n < 254 then
-- Note trigger
N = math.floor(v.n / 0x10) * 12 + (v.n % 0x10)
end
else
-- No note
v.n = 0xFF
N = false
end
if v.i then
if v.i == 0 then
-- Instrument undefined
v.currInstrument = 0
I = false
elseif v.i > 0 then
-- Instrument defined
v.currInstrument = v.i
I = v.i - 1
end
else
-- No instrument
v.i = 0
I = false
end
if v.v then
V = v.v
else
-- No volume
v.v = 0
V = false
end
if v.c then
v.fxCommand = v.c
v.fxData = v.d
else
-- No effect
v.c = 0
v.d = 0
end
C = string.char(v.c + 0x40)
D = v.d
Dx = math.floor(D / 16)
Dy = D % 16
-- Early note delay detection...
if currentTick == 0 then
local x = math.floor(D / 0x10)
if C == 'S' and x == 0xD then
-- Note Delay
local y = D % 0x10
-- No % -> delay may happen across rows, which is probably wrong.
v.noteDelayTicks = y % speed
v.noteDelayTrigger = true
elseif v.noteDelayTicks > 0 then
-- Reset delay ticks since it was more than the speed value.
-- Removing this needed to allow delays across rows, as said above.
v.noteDelayTicks = 0
v.noteDelayTrigger = false
end
end
if (not v.noteDelayTrigger and currentTick == 0) or
( v.noteDelayTrigger and v.noteDelayTicks == 0) then
v.noteDelayTrigger = false
-- Combinatorics...
if N and I then
-- Apply instrument
v.instrument = module.sample[I]
if C ~= 'G' then
-- Set note and reset offset to 0.
v:setPeriod(N)
v.currOffset = 0
end
-- Handle volume
if V then
v.currVolume = V / 0x40
else
if v.instrument and v.instrument.volume then
v.currVolume = v.instrument.volume / 0x40
end
end
elseif N and not I then
if C ~= 'G' then
-- Set note and reset offset to 0.
v:setPeriod(N)
v.currOffset = 0
end
-- Handle volume
if V then
v.currVolume = V / 0x40
else
-- Do nothing here.
end
elseif not N and I then
-- Apply instrument
v.instrument = module.sample[I]
-- Handle volume
if V then
v.currVolume = V / 0x40
else
if v.instrument then
v.currVolume = v.instrument.volume / 0x40
end
end
elseif not N and not I then
-- Handle volume
if V then
v.currVolume = V / 0x40
else
-- Do nothing here.
end
end
if N then v.lastNote = N end
end
if currentTick == 0 then
-- Hack: If C is @ it means only the data has been saved and that
-- there's no effect.
if C == '@' then
if D ~= 0x00 then
v.fxSlotGeneric = D
end
end
-- T0 Effects.
if C == 'D' then
-- Volume SLide
if D > 0x00 then
v.fxSlotGeneric = D
end
local x = math.floor(v.fxSlotGeneric / 0x10)
local y = v.fxSlotGeneric % 0x10
-- If we have a fine slide, then process it here.
-- Note that in the case of DFF, we prioritize by fine sliding up.
if y == 0xF then
-- up x units
v.currVolume = math.min(1.0, v.currVolume + (x / 0x40))
elseif x == 0xF then
-- down y untis
v.currVolume = math.max(0.0, v.currVolume - (y / 0x40))
end
-- "Fast Volume Slide" bug handling
if module.fastVolSlides then
if y == 0x0 then
-- up x units
v.currVolume = math.min(1.0, v.currVolume + (x / 0x40))
elseif x == 0x0 then
-- down y units
v.currVolume = math.max(0.0, v.currVolume - (y / 0x40))
end
end
elseif C == 'E' then
-- Portamento Down
if D > 0x00 then
v.fxSlotPortamento = D
end
local x = math.floor(v.fxSlotPortamento / 0x10)
local y = v.fxSlotPortamento % 0x10
if x == 0xF then
-- Fine porta
v.instPeriod = v.instPeriod + y * 4
elseif x == 0xE then
-- Extra fine porta
v.instPeriod = v.instPeriod + y
end
--v.instPeriod = math.min(v.instPeriod, 27392)
elseif C == 'F' then
-- Portamento Up
if D > 0x00 then
v.fxSlotPortamento = D
end
local x = math.floor(v.fxSlotPortamento / 0x10)
local y = v.fxSlotPortamento % 0x10
if x == 0xF then
-- Fine porta
v.instPeriod = v.instPeriod - y * 4
elseif x == 0xE then
-- Extra fine porta
v.instPeriod = v.instPeriod - y
end
-- Amiga limits
if module.amigaNoteLimits then
v.instPeriod = math.max(v.instPeriod, 56)
end
v.instPeriod = math.max(v.instPeriod, 0)
elseif C == 'G' then
-- Tone portamento
if D > 0x00 then
v.fxSlotPortamento = D
end
if N and v.instrument and v.instrument.c4speed then
v.glisPeriod = NOTEPERIOD[N] *
(DEFAULTC4SPEED / v.instrument.c4speed)
end
elseif C == 'H' then
-- Vibrato
local x = math.floor(D / 0x10)
local y = D % 0x10
-- TODO: Some modules imply that the two param parts are set
-- separately.
if x > 0x0 then
v.fxSlotVibrato = (x * 0x10) + (v.fxSlotVibrato % 0x10)
end
if y > 0x0 then
v.fxSlotVibrato = math.floor(v.fxSlotVibrato / 0x10) * 0x10 + y
end
-- If wavecontrol is retriggering, then reset offset here.
if N and v.vibratoWaveform < 4 then
v.vibratoOffset = 32
end
elseif C == 'I' then
-- Tremor
if D > 0x00 then
v.fxSlotGeneric = D
v.tremorOnTicks = math.floor(v.fxSlotGeneric / 0x10)
v.tremorOffTicks = v.fxSlotGeneric % 0x10
end
-- The actual function
-- OpenMPT implements this with both x0 and x1 and 0y and 1y being counted as on/off for 1 tick (except 00),
-- But FireLight claims it's x+1, y+1 all the way (except 00)...
-- TEST THIS MORE! (Also whether the internal counter gets reset at anytime or not... row/pat/song)
--local x, y = v.tremorOnTicks+1, v.tremorOffTicks+1
local x, y = v.tremorOnTicks, v.tremorOffTicks
if x == 0 and y == 0 then
-- Use previous values (S3M "bug" if this wasn't here and we set 00 to 11.)
else
-- Adjust values
x = v.tremorOnTicks == 0 and v.tremorOnTicks + 1 or v.tremorOnTicks
y = v.tremorOffTicks == 0 and v.tremorOffTicks + 1 or v.tremorOffTicks
end
v.tremorOffset = v.tremorOffset % (x + y) -- sum is 32 maximum
if v.tremorOffset >= x then
print('T0 off ' .. v.tremorOffset)
v.currVolume = 0
else
print('T0 on ' .. v.tremorOffset)
v.currVolume = (V and V or v.instrument.volume) / 0x40
end
v.tremorOffset = v.tremorOffset + 1
elseif C == 'J' then
-- Arpeggio
if D > 0x00 then
v.fxSlotGeneric = D
v.arpOffset[1] = math.floor(v.fxSlotGeneric / 0x10)
v.arpOffset[2] = v.fxSlotGeneric % 0x10
end
elseif C == 'K' then
-- VolSlide + Vibrato
if D > 0x00 then
v.fxSlotGeneric = D -- Sets the volume slide params only.
end
elseif C == 'L' then
-- VolSlide + TonePorta
if D > 0x00 then
v.fxSlotGeneric = D -- Sets the volume slide params only.
end
elseif C == 'O' then
-- Set Offset
v.fxSlotGeneric = D
v.setOffset = D * 0x100
elseif C == 'Q' then
-- Retrigger note (+VolSlide)
if D > 0x00 then
v.fxSlotGeneric = D
end
elseif C == 'R' then
-- Tremolo
if D > 0x00 then
v.fxSlotGeneric = D -- Rxy goes into generic slot.
end
-- If wavecontrol is retriggering, then reset offset here.
if N and v.tremoloWaveform < 4 then
v.tremoloOffset = 32
end
elseif C == 'S' then
local x = math.floor(D / 0x10)
if x == 0x1 then
-- Glissando
local y = D % 0x10
v.glissando = not (y == 0) -- If true, slide by semitones.
elseif x == 0x2 then
-- Set FineTune
-- TODO: This seemingly destroys any other previous value for
-- c4speed; check if this is actually how it should work.
-- IDEA: Maybe modify the base c4speed instead?
if v.instrument then
v.instrument.c4speed = C4SPEEDFINETUNES[D % 0x10]
end
elseif x == 0x3 then
-- Set Vibrato Waveform
-- TODO: See if this is global or per-channel - seems to be per-channel.
local y = D % 0x10
if y < 8 then
v.vibratoWaveform = y
end
elseif x == 0x4 then
-- Set Tremolo Waveform
-- TODO: See if this is global or per-channel - seems to be per-channel.
local y = D % 0x10
if y < 8 then
v.tremoloWaveform = y
end
elseif x == 0x8 then
-- Set Panning (unsigned)
local y = D % 0x10
v.currPanning = y / 0x10
elseif x == 0xA then
-- Stereo Control (signed) - ST3's help screen says this is "old".
local y = D % 0x10
y = y > 7 and y - 16 or y
v.currPanning = (8 + y) / 0xF
elseif x == 0xC then
-- Note Cut
local y = D % 0x10
-- No % -> cut may happen across rows, which is probably wrong.
v.noteCutTicks = y % speed
elseif x == 0xD then
-- The setter part of this needs to be handled before handling
-- the note/instrument/volume columns, AND it needs to be
-- processed on the 0th tick too, because of the code written.
-- Note Delay
if v.noteDelayTicks > 0 then
v.noteDelayTicks = v.noteDelayTicks - 1
end
elseif x == 0xF then
-- Invert Loop OR Funk Repeat
-- Thing is, there are a few possibilities here;
-- A. Implement Invert Loop, which irreversibly modifies the
-- waveform data.
-- B. Implement Funk Repeat, which only works on instruments
-- with very specific settings.
-- C. Implement it differently, e.g. looping will be reversed
-- To be honest, it really is the best solution to just not.
-- Though Scream Tracker V3.21 does state that SFx does stand
-- for FunkRepeat, with x = speed.
end
elseif C == 'U' then
-- Fine Vibrato
local x = math.floor(D / 0x10)
local y = D % 0x10
-- TODO: Some modules imply that the two param parts are set
-- separately.
if x > 0x0 then
v.fxSlotVibrato = (x * 0x10) + (v.fxSlotVibrato % 0x10)
end
if y > 0x0 then
v.fxSlotVibrato = math.floor(v.fxSlotVibrato / 0x10) * 0x10 + y
end
-- If wavecontrol is retriggering, then reset offset here.
if N and v.vibratoWaveform < 4 then
v.vibratoOffset = 32
end
end
else
-- Tn Effects.
if C == 'D' then
-- VolSLide
local x = math.floor(v.fxSlotGeneric / 0x10)
local y = v.fxSlotGeneric % 0x10
if y == 0x0 then
-- up x units
v.currVolume = math.min(1.0, v.currVolume + (x / 0x40))
elseif x == 0x0 then
-- down y units
v.currVolume = math.max(0.0, v.currVolume - (y / 0x40))
end
elseif C == 'E' then
-- Portamento Down
local x = math.floor(v.fxSlotPortamento / 0x10)
if x < 0xE then
v.instPeriod = v.instPeriod + v.fxSlotPortamento * 4
end
--v.instPeriod = math.min(v.instPeriod, 27392)
elseif C == 'F' then
-- Portamento Up
local x = math.floor(v.fxSlotPortamento / 0x10)
if x < 0xE then
v.instPeriod = v.instPeriod - v.fxSlotPortamento * 4
end
v.instPeriod = math.max(v.instPeriod, 0)
elseif C == 'G' then
-- Tone Portamento
if v.tempPeriod > v.glisPeriod then
v.tempPeriod = v.tempPeriod - v.fxSlotPortamento * 4
if v.tempPeriod < v.glisPeriod then
v.tempPeriod = v.glisPeriod
end
elseif v.tempPeriod < v.glisPeriod then
v.tempPeriod = v.tempPeriod + v.fxSlotPortamento * 4
if v.tempPeriod > v.glisPeriod then
v.tempPeriod = v.glisPeriod
end
end
if not v.glissando then
v.instPeriod = v.tempPeriod
else
-- This works, though it's not exact to either ST3 not OpenMPT.
if v.tempPeriod > v.glisPeriod then
local p = PERIODBINSEARCH(v.tempPeriod)-1
p = math.max(p, 0)
v.instPeriod = NOTEPERIOD[p]
--* (DEFAULTC4SPEED / v.instrument.c4speed)
elseif v.tempPeriod < v.glisPeriod then
local p = PERIODBINSEARCH(v.tempPeriod)+1
p = math.min(p, 131)
v.instPeriod = NOTEPERIOD[p]
--* (DEFAULTC4SPEED / v.instrument.c4speed)
end
end
elseif C == 'H' then
-- Vibrato
local pos = v.vibratoOffset - 32 -- [0,63] -> [-32,31]
local speed = math.floor(v.fxSlotVibrato / 0x10)
local depth = v.fxSlotVibrato % 0x10
local delta = WAVEFORMTABLE[v.vibratoWaveform][v.vibratoOffset]
--delta = delta * depth
--delta = delta / 128
--delta = delta * 4 -- Fine vibrato is the unmultiplied one
delta = delta * depth / 32
if pos < 0 then
v.vibratoFreqDelta = -delta
else
v.vibratoFreqDelta = delta
end
v.vibratoOffset = (v.vibratoOffset + speed) % 64
elseif C == 'I' then
-- Tremor
-- The actual function
-- OpenMPT implements this with both x0 and x1 and 0y and 1y being counted as on/off for 1 tick (except 00),
-- But FireLight claims it's x+1, y+1 all the way (except 00)...
--local x, y = v.tremorOnTicks+1, v.tremorOffTicks+1
local x, y = v.tremorOnTicks, v.tremorOffTicks
if x == 0 and y == 0 then
-- Use previous values (S3M "bug" if this wasn't here and we set 00 to 11.)
else
-- Adjust values
x = v.tremorOnTicks == 0 and v.tremorOnTicks + 1 or v.tremorOnTicks
y = v.tremorOffTicks == 0 and v.tremorOffTicks + 1 or v.tremorOffTicks
end
v.tremorOffset = v.tremorOffset % (x + y) -- sum is 32 maximum
if v.tremorOffset >= x then
print('Tn off ' .. v.tremorOffset)
v.currVolume = 0
else
print('Tn on ' .. v.tremorOffset)
v.currVolume = (V and V or v.instrument.volume) / 0x40
end
v.tremorOffset = v.tremorOffset + 1
elseif C == 'J' then
-- Arpeggio
-- The below code is how the effect would work if ST3/S3M didn't
-- use a constant 50Hz rate for the effect.
--v.arpIndex = currentTick % 3
--v:setPeriod(math.min(v.lastNote + v.arpOffset[v.arpIndex], 131))
elseif C == 'K' then
-- Vibrato
local pos = v.vibratoOffset - 32 -- [0,63] -> [-32,31]
local speed = math.floor(v.fxSlotVibrato / 0x10)
local depth = v.fxSlotVibrato % 0x10
local delta = WAVEFORMTABLE[v.vibratoWaveform][v.vibratoOffset]
--delta = delta * depth
--delta = delta / 128
--delta = delta * 4 -- Fine vibrato is the unmultiplied one
delta = delta * depth / 32
if pos < 0 then
v.vibratoFreqDelta = -delta
else
v.vibratoFreqDelta = delta
end
v.vibratoOffset = (v.vibratoOffset + speed) % 64
-- VolSlide
local x = math.floor(v.fxSlotGeneric / 0x10)
local y = v.fxSlotGeneric % 0x10
if y == 0x0 then
-- up x units
v.currVolume = math.min(1.0, v.currVolume + (x / 0x40))
elseif x == 0x0 then
-- down y units
v.currVolume = math.max(0.0, v.currVolume - (y / 0x40))
end
elseif C == 'L' then
-- TonePorta
if v.tempPeriod > v.glisPeriod then
v.tempPeriod = v.tempPeriod - v.fxSlotPortamento * 4
if v.tempPeriod < v.glisPeriod then
v.tempPeriod = v.glisPeriod
end
elseif v.tempPeriod < v.glisPeriod then
v.tempPeriod = v.tempPeriod + v.fxSlotPortamento * 4
if v.tempPeriod > v.glisPeriod then
v.tempPeriod = v.glisPeriod
end
end
if not v.glissando then
v.instPeriod = v.tempPeriod
else
-- This works, though it's not exact to either ST3 not OpenMPT.
if v.tempPeriod > v.glisPeriod then
local p = PERIODBINSEARCH(v.tempPeriod)-1
p = math.max(p, 0)
v.instPeriod = NOTEPERIOD[p]
--* (DEFAULTC4SPEED / v.instrument.c4speed)
elseif v.tempPeriod < v.glisPeriod then
local p = PERIODBINSEARCH(v.tempPeriod)+1
p = math.min(p, 131)
v.instPeriod = NOTEPERIOD[p]
--* (DEFAULTC4SPEED / v.instrument.c4speed)
end
end
-- VolSlide
local x = math.floor(v.fxSlotGeneric / 0x10)
local y = v.fxSlotGeneric % 0x10
if y == 0x0 then
-- up x units
v.currVolume = math.min(1.0, v.currVolume + (x / 0x40))
elseif x == 0x0 then
-- down y units
v.currVolume = math.max(0.0, v.currVolume - (y / 0x40))
end
elseif C == 'Q' then
-- Retrigger note (+VolSlide)
local x = math.floor(v.fxSlotGeneric / 0x10)
local y = v.fxSlotGeneric % 0x10
v.currVolume = math.floor(
RETRIGVOLSLIDEFUNC[x](v.currVolume * 0x40)) / 0x40
v.currVolume = math.min(math.max(v.currVolume, 0.0), 1.0)
if currentTick % y == 0 then
v.currOffset = 0
if V then
v.currVolume = V / 0x40
elseif I then
v.currVolume = v.instrument.volume / 0x40
end
end
elseif C == 'R' then
-- Tremolo
local pos = math.abs(v.tremoloOffset) - 32 -- [0,63] -> [-32,31]
local speed = math.floor(v.fxSlotGeneric / 0x10)
local depth = v.fxSlotGeneric % 0x10
local delta = WAVEFORMTABLE[v.tremoloWaveform][v.tremoloOffset]
--delta = delta * depth
--delta = delta / 64
--delta = delta * 4
delta = delta * depth / 16
if pos < 32 then
v.currVolume = math.min(v.currVolume + (delta / 0x40), 1)
else
v.currVolume = math.max(v.currVolume - (delta / 0x40), 1)
end
v.tremoloOffset = (v.tremoloOffset + speed) % 64
elseif C == 'S' then
local x = math.floor(D / 0x10)
if x == 0xC then
-- Note Cut
-- This code also works for the case when
-- noteCutTicks >= speed
if v.noteCutTicks > 0 then
v.noteCutTicks = v.noteCutTicks - 1
if v.noteCutTicks == 0 then
v.currVolume = 0.0
end
end
elseif x == 0xD then
-- Note Delay
if v.noteDelayTicks > 0 then
v.noteDelayTicks = v.noteDelayTicks - 1
end
end
elseif C == 'U' then
-- Fine Vibrato
local pos = v.vibratoOffset - 32 -- [0,63] -> [-32,31]
local speed = math.floor(v.fxSlotVibrato / 0x10)
local depth = v.fxSlotVibrato % 0x10
local delta = WAVEFORMTABLE[v.vibratoWaveform][v.vibratoOffset]
--delta = delta * depth
--delta = delta / 128
--delta = delta * 1
delta = delta * depth / 128
if pos < 0 then
v.vibratoFreqDelta = -delta
else
v.vibratoFreqDelta = delta
end
v.vibratoOffset = (v.vibratoOffset + speed) % 64
end
end
end
Voice.render = function(v)
local smpL, smpR = 0.0, 0.0
if v.instPeriod == 0 then return smpL, smpR end
if not v.instrument or v.instrument.type == 0 then return smpL, smpR end
if v.instrument.type == 1 then
-- Sampler.
local freq
-- ST3 Arpeggio
if v.c == 0x0A then
v.arpIndex = (v.arpIndex + (samplingPeriod / ARPEGGIOPERIOD)) % 3
v:setPeriod(
math.min(v.arpOffset[math.floor(v.arpIndex)] + v.lastNote,
131))
end
-- Vibrato
if v.c == 0x08 or v.c == 0x0B or v.c == 0x16 then
freq = v.instPeriod + v.vibratoFreqDelta
else
freq = v.instPeriod
end
v.currOffset = v.currOffset + (FIXEDCLOCK / freq)
if v.setOffset > 0 then
-- Add setOffset parameter.
v.currOffset = v.currOffset + v.setOffset
v.setOffset = 0
end
if v.instrument.looped then
local addend = v.currOffset - v.instrument.loopEnd
if addend >= 0 then
v.currOffset = v.instrument.loopStart + addend
end
else
if v.currOffset > v.instrument.data:getSampleCount() *
v.instrument.data:getChannelCount()
then
v.currOffset = 0.0
v.instPeriod = 0 -- Only play the sample once.
return smpL, smpR
end
end
v.currOffset = v.currOffset % (v.instrument.data:getSampleCount() *
v.instrument.data:getChannelCount())
-- Interpolation
if interpolation == 'nearest' then
-- 0th order interpolation: nearest neighbour (piecewise constant)
if v.instrument.channelCount == 1 then
local p = math.floor(v.currOffset)
smpL = v.instrument.data:getSample(p)
smpR = smpL
else
-- Stereo is not standard ST3, but implementable.
local p = math.floor(v.currOffset)
p = p % 2 == 1 and p - 1 or p
smpL = v.instrument.data:getSample(p)
smpR = v.instrument.data:getSample(p + 1)
end
elseif interpolation == 'linear' then
-- TODO
end
smpL = smpL * v.currVolume * (1.0 - v.currPanning)
smpR = smpR * v.currVolume * v.currPanning
return smpL, smpR
elseif v.instrument.type == 2 then
-- TODO: AdLib OPL2 synth - melodics.
return smpL, smpR
end
end
local mtVoice = {__index = Voice}
Voice.new = function(ch, pan)
local v = setmetatable({}, mtVoice)
v.ch = ch
-- Processing related.
v.disabled = false -- Whether or not the voice is processed.
v.muted = false -- Whether or not the voice output is muted.
-- Per-row input data.
v.n, v.i, v.v, v.c, v.d = 0xFF, 0x00, 0x00, 0x00, 0x00
-- Reference to the instrument
v.instrument = false -- Reference to the current instrument.
-- Current running values
v.lastNote = 0
v.notePeriod = 0x0000 -- Base period value as taken from note data.
v.glisPeriod = 0x0000 -- Final (true) period value of Gxx glissando effects.
v.instPeriod = 0x0000 -- True period value calc.-ed w/ the current instrument.
v.currOffset = 0.0 -- Current sample offset. (floored -> matrix displayable)
v.currVolume = 0.0 -- Current volume.
v.currPanning = pan / 0xF -- Current panning.
v.currInstrument = 0x00 -- Only for display purposes.
-- Emulate ST3 limited effect memory.
v.fxCommand = 0x00 -- Effect command.
v.fxData = 0x00 -- Effect parameter.
v.fxSlotGeneric = 0x00 -- Generic effect parameter slot. (D, K*, L, E?, F?, G???, I, J, Q, R, S)
v.fxSlotPortamento = 0x00 -- Portamento effect parameter slot. (E?/F?/G???)
v.fxSlotVibrato = 0x00 -- Vibrato effect parameter slot. (H/U/K*)
-- Faster calculation
v.noteDelayTicks = 0x0 -- Ticks to delay note onsets.
v.noteCutTicks = 0x0 -- Ticks to cut note sound after.
v.noteDelayTrigger = false -- Internal helper.
v.arpIndex = 0x0 -- Running index for arpeggio effect.
v.arpOffset = {}
v.arpOffset[0] = 0x0 -- Arpeggio offsets.
v.arpOffset[1] = 0x0 -- -"-.
v.arpOffset[2] = 0x0 -- -"-.