-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathfortISSimO.asm
2170 lines (1957 loc) · 54.1 KB
/
fortISSimO.asm
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
; If you want to use fortISSimO inside of hUGETracker itself,
; please read the dedicated section in the manual:
; https://eldred.fr/fortISSimO/hugetracker.html
;
; def HUGETRACKER equs "???"
IF DEF(HUGETRACKER)
WARN "\n\tPlease report this issue to fortISSimO, *NOT* hUGETracker!\n\t(Even if it seems unrelated.)\n\t>>> https://github.com/ISSOtm/fortISSimO/issues <<<\n"
IF !STRCMP("{HUGETRACKER}", "1.0b10")
ELIF !STRCMP(STRSUB("{HUGETRACKER}", 1, 3), "1.0")
ELSE
FAIL "Unsupported hUGETracker version \"{HUGETRACKER}\"!"
ENDC
ELIF DEF(PREVIEW_MODE)
; PREVIEW_MODE is defined when assembling for hUGETracker.
; hUGETracker contains a Game Boy emulator (the "G" in "UGE"), and it relies on some cooperation
; from the driver to signal key updates.
; This goes both ways, though: don't try to run PREVIEW_MODE code outside of hUGETracker!
FAIL "fortISSimO is not properly configured for use in hUGETracker!\n\tPlease follow the instructions at the top of hUGEDriver.asm."
ENDC
; Some terminology notes:
; - An "order" is a table of pointers to "patterns".
; - A "pattern" is a series of up to 64 "rows".
; - A "table" is a subpattern
;
; A "TODO: loose" comment means that the `hli` it's attached to could be used as an optimisation point.
IF !DEF(HUGETRACKER)
INCLUDE "hardware.inc" ; Bread & butter: check.
INCLUDE "fortISSimO.inc" ; Get the note constants.
ELSE ; The above files are accessed differently when inside hUGETracker.
INCLUDE "include/hardware.inc"
INCLUDE "include/hUGE.inc"
IF !DEF(FORTISSIMO_INC)
FAIL "It seems that you forgot to overwrite hUGETracker's `hUGE.inc` with `fortISSimO.inc`!"
ENDC
ENDC
; Some configuration, with defaults.
; See https://eldred.fr/fortISSimO/integration.html#tuning-fortissimo for details.
IF !DEF(FORTISSIMO_ROM)
def FORTISSIMO_ROM equs "ROM0"
ENDC
IF !DEF(FORTISSIMO_RAM)
def FORTISSIMO_RAM equs "WRAM0"
ENDC
IF !DEF(FORTISSIMO_PANNING)
def FORTISSIMO_PANNING equ rNR51
ENDC
rev_Check_hardware_inc 4.2
IF DEF(override)
PURGE override
ENDC
MACRO override ; Ensures that fO's symbols don't conflict with pre-defined ones.
REPT _NARG
IF DEF(\1)
PURGE \1
ENDC
shift
ENDR
ENDM
override dbg_var, dbg_action, dbg_log, runtime_assert, unreachable ; Pre-defined ones, if any, may have different semantics.
IF DEF(PRINT_DEBUGFILE)
PRINTLN "@debugfile 1.0.0"
MACRO dbg_var ; <name>, <default value>
DEF DEFAULT_VALUE equs "0"
IF _NARG > 1
redef DEFAULT_VALUE equs "\2"
ENDC
PRINTLN "@var \1 {DEFAULT_VALUE}"
PURGE DEFAULT_VALUE
ENDM
MACRO dbg_action ; <action:str> [, <condition:dbg_expr>]
DEF OFS_FROM_BASE equ @ - {.}
DEF ACTION_COND equs ""
IF _NARG > 1
REDEF ACTION_COND equs "\2"
ENDC
PRINTLN "{.}+{d:OFS_FROM_BASE} x {ACTION_COND}: ", \1
PURGE OFS_FROM_BASE, ACTION_COND
ENDM
ELSE ; If not printing debugfiles to stdout, define the "core" macros as do-nothing.
MACRO dbg_var
ENDM
MACRO dbg_action
ENDM
ENDC
MACRO dbg_log ; <message:dbg_str> [, <condition:dbg_expr>]
DEF MSG equs \1
SHIFT
dbg_action "message \"{MSG}\"", \#
PURGE MSG
ENDM
MACRO runtime_assert ; <condition:dbg_expr> [, <message:dbg_str>]
DEF MSG equs "assert failure"
IF _NARG > 1
REDEF MSG equs \2
ENDC
dbg_action "alert \"{MSG}\"", !(\1)
PURGE MSG
ENDM
MACRO unreachable ; [<message:dbg_str>]
DEF MSG equs "unreachable code reached!"
IF _NARG > 0
REDEF MSG equs \1
ENDC
dbg_action "alert \"In {.}: {MSG}\""
PURGE MSG
ENDM
IF !DEF(FORTISSIMO_LOG)
def FORTISSIMO_LOG equs ""
ELSE
redef FORTISSIMO_LOG equs ",{FORTISSIMO_LOG},"
ENDC
MACRO fO_log ; <category:name>, <message:dbg_str> [, <condition:dbg_expr>]
IF STRIN("{FORTISSIMO_LOG}", ",\1,")
shift
dbg_log \#
ENDC
ENDM
; Note: SDCC's linker is crippled by the lack of alignment support.
; So we can't assume any song data nor RAM variables are aligned, as useful as that would be.
;
; SDCC calling conventions: https://sdcc.sourceforge.net/doc/sdccman.pdf#subsubsection.4.3.5.1
IF STRLEN("{FORTISSIMO_ROM}") != 0
SECTION "Sound Driver", FORTISSIMO_ROM
ENDC
_hUGE_SelectSong:: ; C interface.
; @param de: Pointer to the "song descriptor" to load.
; @destroy af bc de hl
hUGE_SelectSong::
; Kill channels that aren't muted.
; Note that we re-enable the channels right after, to avoid pops when they come back online.
ldh a, [hUGE_MutedChannels]
ld hl, rNR12
ld bc, AUDENV_UP ; B = 0
rra
jr c, .ch1NotOurs
ld [hl], b
ld [hl], c
.ch1NotOurs
rra
jr c, .ch2NotOurs
ld l, LOW(rNR22)
ld [hl], b
ld [hl], c
.ch2NotOurs
rra
jr c, .ch3NotOurs
ld l, LOW(rNR30)
ld [hl], l ; This has bit 7 reset.
ld [hl], h ; This has bit 7 set.
.ch3NotOurs
rra
jr c, .ch4NotOurs
ld l, LOW(rNR42)
ld [hl], b
ld [hl], c
.ch4NotOurs
ld hl, hUGE_LoadedWaveID
ld a, hUGE_NO_WAVE
ld [hli], a
xor a ; Begin by not touching any channels until a note first plays on them.
ldh [hUGE_AllowedChannels], a
; Set arpeggio state to something.
assert hUGE_LoadedWaveID + 1 == wArpState
inc a ; ld a, 1
ld [hli], a
assert wArpState + 1 == wRowTimer
; a = 1
ld [hli], a ; The next tick will switch to a new row.
assert wRowTimer + 1 == wTicksPerRow
ld a, [de]
ld [hli], a
inc de
assert wTicksPerRow + 1 == wLastPatternIdx
ld a, [de]
ld [hli], a
add a, 2
ld b, a ; Cache the size of each order table for later.
inc de
assert wLastPatternIdx + 1 == wDutyInstrs
assert wDutyInstrs + 2 == wWaveInstrs
assert wWaveInstrs + 2 == wNoiseInstrs
assert wNoiseInstrs + 2 == wRoutine
assert wRoutine + 2 == wWaves
assert wWaves + 2 == wRowCatalogHigh
assert wRowCatalogHigh + 1 == wSubpatRowCatalogHigh
ld c, 2 + 2 + 2 + 2 + 2 + 1 + 1
.copyPointers
ld a, [de]
ld [hli], a
inc de
dec c
jr nz, .copyPointers
assert wSubpatRowCatalogHigh + 1 == wOrderIdx
IF DEF(PREVIEW_MODE)
; The tracker writes the starting order, but `wForceRow` will cause it to increase.
assert wOrderIdx == current_order
ld a, [hl]
sub 2
ELSE
; Begin at order 0, but `wForceRow` will cause it to increase.
ld a, -2
ENDC
ld [hli], a
assert wOrderIdx + 1 == wPatternIdx
inc hl ; No need to init that, it will be set from `wForceRow`.
assert wPatternIdx + 1 == wForceRow
assert PATTERN_LENGTH == 1 << 6, "Pattern length must be a power of 2"
IF DEF(PREVIEW_MODE)
ld a, [row]
or -PATTERN_LENGTH
ELSE
ld a, -PATTERN_LENGTH
ENDC
ld [hli], a ; Begin by forcing row 0.
; Time to init the channels!
assert wForceRow + 1 == wCH1
ld c, 4
.initChannel
assert wCH1 == wCH1.order
; Copy the order pointer.
ld a, e
ld [hli], a
add a, b
ld e, a
ld a, d
ld [hli], a
adc a, 0
ld d, a
assert wCH1.order + 2 == wCH1.fxParams
inc hl ; Skip FX params.
assert wCH1.fxParams + 1 == wCH1.instrAndFX
; The FX is checked on the first tick for whether it is a vibrato; set it to 0, which is not that.
assert FX_VIBRATO != 0
xor a
ld [hli], a
assert wCH1.instrAndFX + 1 == wCH1.note
inc hl ; Skip note ID.
assert wCH1.note + 1 == wCH1.subPattern
; To ensure that nothing bad happens if a note isn't played on the first row, set the subpattern
; pointer to NULL.
; xor a ; a is already 0.
ld [hli], a
ld [hli], a
assert wCH1.subPattern + 2 == wCH1.subPatternRow
; Although strictly speaking, init'ing the subpattern row is unnecessary, it's still read before
; the NULL check is performed; doing this silences any spurious "uninit'd RAM read" exceptions.
ld [hli], a
assert wCH1.subPatternRow + 1 == wCH1.lengthBit
; Same as above.
ld [hli], a
; Then, we have the 4 channel-dependent bytes
; (period + (porta target / vib counter) / LFSR width + polynom + padding); they don't need init.
ld a, l
add a, 5
ld l, a
adc a, h
sub l
ld h, a
assert wCH1.lengthBit + 1 + 5 == wCH2
dec c ; Are we done?
jr nz, .initChannel
ret
_hUGE_TickSound:: ; C interface.
hUGE_TickSound::
; Disable all muted channels.
ld hl, hUGE_MutedChannels
ld a, [hli]
assert hUGE_MutedChannels + 1 == hUGE_AllowedChannels
cpl
and [hl]
ld [hl], a
ld hl, wArpState
dec [hl]
jr nz, :+
ld [hl], 3
:
inc hl
assert wArpState + 1 == wRowTimer
; Check if we should switch to a new row, or just update "continuous" effects.
dec [hl]
jp nz, ContinueFx
;; This is the first tick; switch to the next row, and reload all pointers.
; Reload delay.
ld a, [wTicksPerRow]
ld [hli], a ; TODO: loose
; Check if there is a row or pattern break, and act accordingly:
; Pattern break + row break at the same time must switch to row R on pattern P!
; But row break on last row must not change the pattern.
; Switch to next row.
ld hl, wForceRow
ld a, [hld]
assert wForceRow - 1 == wPatternIdx
and a
jr nz, .forceRow
inc [hl]
jr nz, .samePattern
; Reload index.
assert PATTERN_LENGTH == 1 << 6, "Pattern length must be a power of 2"
ld a, -PATTERN_LENGTH ; pow2 is required to be able to mask off these two bits.
.forceRow
ld [hld], a
IF DEF(PREVIEW_MODE)
jr nz, .incRequired ; Everything that sets `wForceRow` expects the order to advance.
inc hl
; If looping is enabled, don't switch patterns.
ld a, [loop_order]
and a
jr nz, .samePattern
dec hl
.incRequired
ENDC
; Switch to next patterns.
assert wPatternIdx - 1 == wOrderIdx
ld a, [wLastPatternIdx]
sub [hl]
jr z, .wrapOrders ; Reached end of orders, start again from the beginning.
ld a, [hl]
assert ORDER_WIDTH == 2
inc a
inc a
.wrapOrders
ld [hli], a
assert wOrderIdx + 1 == wPatternIdx
IF DEF(PREVIEW_MODE)
db $fc ; Signal the tracker to refresh the order index.
ENDC
.samePattern
; Compute the offset into the pattern.
ld a, [hli]
assert PATTERN_LENGTH == 1 << 6, "Pattern length must be a power of 2"
and PATTERN_LENGTH - 1
fO_log row_idx, "=== Order row \{[wOrderIdx] / 2 + 1\}, row \{a\} ==="
ld b, a
; Reset the "force row" byte.
assert wPatternIdx + 1 == wForceRow
xor a
ld [hli], a
;; Play new rows.
; Note that all of these functions leave b untouched all the way until CH4's `ReadRow`!
; If the previous FX was not a vibrato, set the "vibrato arg" to 0.
; Note that all of these should run on the song's first tick, which initialises `.vibratoPrevArg`
; if the first row has a vibrato, and avoids reading uninit'd RAM.
; Note also that these are all run before `RunTick0Fx`, which can write to the overlapping `.portaTarget`,
; and before `ReadRow`, which will overwrite `.instrAndFx`.
ld a, [wCH1.instrAndFX]
and $0F
cp FX_VIBRATO
jr z, .ch1WasNotVibrato
xor a
ld [wCH1.vibratoPrevArg], a
.ch1WasNotVibrato
ld a, [wCH2.instrAndFX]
and $0F
cp FX_VIBRATO
jr z, .ch2WasNotVibrato
xor a
ld [wCH2.vibratoPrevArg], a
.ch2WasNotVibrato
ld a, [wCH3.instrAndFX]
and $0F
cp FX_VIBRATO
jr z, .ch3WasNotVibrato
xor a
ld [wCH3.vibratoPrevArg], a
.ch3WasNotVibrato
; CH4 does not support vibrato, so it's not checked.
assert wForceRow + 1 == wCH1.order
; ld hl, wCH1.order
call ReadRow
ld hl, wCH1.instrAndFX
ld e, hUGE_CH1_MASK
ld c, LOW(rNR10)
call nz, PlayDutyNote
ld hl, wCH2.order
call ReadRow
ld hl, wCH2.instrAndFX
ld e, hUGE_CH2_MASK
ld c, LOW(rNR21 - 1) ; NR20 doesn't exist.
call nz, PlayDutyNote
ld hl, wCH3.order
call ReadRow
call nz, PlayWaveNote
ld hl, wCH4.order
call ReadRow
call nz, PlayNoiseNote
;; Process tick 0 FX and subpatterns.
ld de, wCH1.fxParams
ld c, hUGE_CH1_MASK
call RunTick0Fx
ld hl, wCH1.lengthBit
ld c, hUGE_CH1_MASK
call TickSubpattern
ld de, wCH2.fxParams
ld c, hUGE_CH2_MASK
call RunTick0Fx
ld hl, wCH2.lengthBit
ld c, hUGE_CH2_MASK
call TickSubpattern
ld de, wCH3.fxParams
ld c, hUGE_CH3_MASK
call RunTick0Fx
ld hl, wCH3.lengthBit
ld c, hUGE_CH3_MASK
call TickSubpattern
ld de, wCH4.fxParams
ld c, hUGE_CH4_MASK
call RunTick0Fx
ld hl, wCH4.lengthBit
ld c, hUGE_CH4_MASK
assert @ == TickSubpattern ; fallthrough
; @param hl: Pointer to the channel's length bit.
; @param c: The channel's mask (the CHx_MASK constant).
; @destroy a bc de hl (potentially)
TickSubpattern:
ld a, [hld] ; Read the length bit.
ld b, a
assert wCH1.lengthBit - 1 == wCH1.subPatternRow
runtime_assert [@hl] < 32, "Subpattern row index out of bounds! (\{[@hl]\})"
ld a, [hld]
ld e, a
assert wCH1.subPatternRow - 2 == wCH1.subPattern ; 16-bit variable.
; Add the row offset to the subpattern base pointer.
ld a, [hld]
ld d, a
or [hl]
ret z ; Return if subpattern pointer is NULL (no subpattern).
ld a, [hld]
assert wCH1.subPattern - 1 == wCH1.note
push hl ; Save pointer to current note.
add a, e
ld l, a
adc a, d
sub l
ld h, a
; Read the row's ID, and compute the pointer to it.
ld l, [hl]
ld a, [wSubpatRowCatalogHigh]
ld h, a
fO_log subpat_row, "CH\{c,2\} reading subpattern row from $\{hl,04$\}: (\{[hl + 512],2\}, \{[hl + 256],2$\}_\{[hl],2$\})"
; Read the row's FX parameter.
ld a, [hl]
ldh [hUGE_FxParam], a
inc h
runtime_assert [(([@hl] & $0F) * 2 + TickSubpattern.fxPointers)!] != KnownRet, "Bad command (\{[@hl],$\}) in subpattern!"
ld a, [hl] ; Read the jump target and FX ID.
inc h
ld l, [hl] ; Read the note offset.
IF DEF(HUGETRACKER)
rlc l ; We can't store the offset pre-rotated because hT uses a single `dn` macro.
ENDC
ld h, a ; We'll need to persist this for a bit.
; Update the index to point to the next row.
and $F0 ; Retain the jump target only.
; There is one extra bit (bit 4) in the note field, specifically its bit 0.
srl l ; Move the extra bit into carry, and put the note in place.
adc a, 0 ; Inject the extra bit into bit 0.
swap a ; Put the bits in their right place.
pop de ; This points to `wCHx.note`.
assert wCH1.subPatternRow - wCH1.note == 3
inc de
inc de
inc de
IF DEF(HUGETRACKER) ; We don't normally do this, because it introduces an overflow bug.
jr nz, .jump
ld a, [de]
inc a
and 32 - 1 ; Subpatterns wrap around.
db $FE ; Swallow up next byte.
.jump
dec a
ENDC
ld [de], a
; Apply the note offset, if any.
dec de ; Point back to the base note.
dec de
dec de
ld a, l
cp LAST_NOTE
jr nc, .noNoteOffset
; Check if the channel is muted; if so, don't write to NRxy.
ldh a, [hUGE_AllowedChannels]
and c
jr z, .noNoteOffset
; Compute the note's ID.
ld a, [de]
add a, l
sub LAST_NOTE / 2 ; Go from "unsigned range" to "signed range".
runtime_assert @a < {LAST_NOTE}, "Subpattern offset over/underflowed note ID! \{@a\}"
bit 3, c
assert hUGE_CH4_MASK == 1 << 3
jr nz, .ch4
; For the FX dispatch below, we need the FX ID (in `h`) and the channel mask (in `c`).
ld l, c
push hl
push de
ld hl, wCH1.period - wCH1.note
add hl, de
; Compute the note's period.
add a, a
add a, LOW(PeriodTable)
ld e, a
adc a, HIGH(PeriodTable)
sub e
ld d, a
; Compute the pointer to NRx3, bit twiddling courtesy of @calc84maniac.
ld a, c ; a = 1 (CH1), 2 (CH2), or 4 (CH3).
xor $11 ; 10, 13, 15
add a, c ; 11, 15, 19
cp LOW(rNR23)
adc a, c ; 13, 18, 1D
ld c, a
; Write the period, together with the length bit.
ld a, [de]
ld [hli], a
ldh [c], a
inc c
inc de
ld a, [de]
ld [hl], a
or b ; Add the length bit.
ldh [c], a
pop de ; Restore the pointer to the channel's note ID (for FX).
pop bc ; Restore the FX ID and the channel mask.
.appliedOffset
; Play the row's FX.
ld a, b ; Read the FX/instr byte again.
and $0F ; Keep the FX bits only.
add a, a
add a, LOW(.fxPointers)
ld l, a
adc a, HIGH(.fxPointers)
sub l
ld h, a
; Retrieve the FX param.
ldh a, [hUGE_FxParam]
ld b, a
; Deref the pointer, and jump to it.
ld a, [hli]
ld h, [hl]
ld l, a
jp hl
.ch4
call GetNoisePolynom
ld d, a
ld [wCH4.polynom], a
ld a, [wCH4.lfsrWidth]
or d
ldh [rNR43], a
ld a, b
ldh [rNR44], a
ld de, wCH4.note ; Restore the pointer to the channel's note ID (for FX).
.noNoteOffset
ld b, h ; Transfer the FX ID for the calling code.
jr .appliedOffset
.fxPointers
dw FxArpeggio
dw FxPortaUp
dw FxPortaDown
dw KnownRet ; No tone porta
dw KnownRet ; No vibrato
dw FxSetMasterVolume
dw FxCallRoutine
dw FxFixedMode ; Repurposed note delay
dw FxSetPanning
dw FxChangeTimbre
dw FxVolumeSlide
dw KnownRet ; No pos jump
dw FxSetVolume
dw KnownRet ; No pattern break
dw KnownRet ; No note cut
dw KnownRet ; This would reset the row timer, and you DEFINITELY don't want that.
; @param hl: Pointer to the channel's order pointer.
; @param b: Offset into the current patterns.
; @return hl: Pointer to the channel's period (1, 2, 3) / polynom (4).
; @return c: New row's instrument/FX byte.
; @return d: New row's note index.
; @return zero: If set, the note should not be played.
; @destroy e a
ReadRow:
; Compute the pointer to the current pattern.
ld a, [wOrderIdx] ; TODO: cache this in a reg across calls?
add a, [hl]
ld e, a
inc hl
ld a, [hli]
adc a, 0
ld d, a
assert wCH1.order + 2 == wCH1.fxParams
; Compute the pointer to the current row.
ld a, [de]
add a, b
ld c, a
inc de
ld a, [de]
adc a, 0
ld d, a
ld e, c
; Read the row's ID, and compute the pointer to the actual row.
ld a, [de]
ld e, a
ld a, [wRowCatalogHigh]
ld d, a
; Read the row into the channel's data.
fO_log main_row, "CH\{((hl - wCH1.fxParams) / (wCH2 - wCH1)) + 1\} reading row from $\{de,04$\}: (\{[de + 512],2\}, \{[de + 256],2$\}_\{[de],2$\})"
ld a, [de]
ld [hli], a
inc d
assert wCH1.fxParams + 1 == wCH1.instrAndFX
ld a, [de]
ld [hli], a
ld c, a
inc d
assert wCH1.instrAndFX + 1 == wCH1.note
ld a, [de]
runtime_assert a < {d:LAST_NOTE} || a == {d:___}, "Invalid note ID \{a,#\}"
ld d, a
; If the row is a rest, don't play it.
cp ___
ret z
ld [hli], a ; Only write the note back if it's not a rest.
; If the FX is a tone porta or a note delay, don't play the note yet.
ld a, c
assert FX_NOTE_DELAY == FX_TONE_PORTA | $04, "Difference between note delay (${x:FX_NOTE_DELAY}) and tone porta (${x:FX_TONE_PORTA}) must be a single bit"
and $0F & ~$04
cp FX_TONE_PORTA
ret
; @param e: The ID of the wave to load.
; @destroy hl e a
LoadWave:
; Compute a pointer to the wave.
ld a, e
.waveInA
ld [hUGE_LoadedWaveID], a
ld hl, wWaves
add a, [hl]
inc hl
ld h, [hl]
ld l, a
adc a, h
sub l
ld h, a
IF !DEF(FORTISSIMO_CH3_KEEP)
; Temporarily "disconnect" CH3 while loading the wave, to mitigate the DC offset from turning the DAC off.
ldh a, [rNR51]
ld e, a
and ~(AUDTERM_3_LEFT | AUDTERM_3_RIGHT)
ldh [rNR51], a
ENDC
; Load the wave.
xor a
ldh [rNR30], a ; Disable CH3's DAC while loading wave RAM.
FOR OFS, 0, 16
ld a, [hli]
ldh [_AUD3WAVERAM + OFS], a
ENDR
ld a, AUD3ENA_ON
ldh [rNR30], a ; Re-enable CH3's DAC.
IF !DEF(FORTISSIMO_CH3_KEEP)
ld a, e
ldh [rNR51], a
ENDC
ret
; Starts playing a new note on the channel, writing back its period to the channel's struct.
; The "standard" comes from CH4 encoding its frequency in a special way, whereas channels 1, 2, and 3
; all work (mostly) the same.
; @param a: ID of the note to play.
; @param c: LOW(rNRx3)
; @param hl: Pointer to the channel's `.period`.
; @destroy c de hl a
def PlayNewNoteStandard equs "PlayDutyNote.playNewNote"
; @param de: Pointer to the channel's FX params.
; @param c: The channel's ID (0 for CH1, 1 for CH2, etc.)
; @destroy a bc de hl (potentially)
RunTick0Fx:
ld a, [de]
ld b, a
inc de
assert wCH1.fxParams + 1 == wCH1.instrAndFX
ld a, [de]
and $0F ; Strip instrument bits.
add a, a ; Each entry in the table is 2 bytes.
add a, LOW(Tick0Fx)
ld l, a
adc a, HIGH(Tick0Fx)
sub l
ld h, a
assert wCH1.instrAndFX + 1 == wCH1.note
inc de
; WARNING: `NoteCutTick0Trampoline` assumes that it's jumped to with `a == h`.
jp hl
; All of the FX functions follow the same calling convention:
; @param de: Pointer to the channel's note byte.
; @param c: The channel's mask (the CHx_MASK constant).
; @param b: The FX's parameters.
; @destroy a bc de hl (potentially)
override No, To
MACRO No ; For "empty" entries in the JR tables.
ret
ds 1
ENDM
DEF NB_TO_PRINT = 0
MACRO To
jr \1
IF DEF(PRINT_JR_STATS)
DEF TO_PRINT{d:NB_TO_PRINT} equs STRCAT(STRRPL("\2", "from ", ""), ",\1")
DEF NB_TO_PRINT += 1
DEF FROM equs STRCAT("From", STRRPL("\2", "from ", ""), "To\1")
{FROM}::
PURGE FROM
ENDC
ENDM
; FX code that only runs on tick 0.
FxChangeTimbre2: ; These are jumped to by `FxChangeTimbre` below.
.ch1
ld a, b
ldh [rNR11], a
ret
.ch2
ld a, b
ldh [rNR21], a
ret
.ch3
ld a, b
swap a ; TODO: optimise this at compile time
call LoadWave.waveInA
; We now need to retrigger the channel, since we had to stop it to reload the wave.
ld hl, wCH3.period + 1
ld a, [hld] ; It's annoying that these bits are there, but that's how it is.
dec hl
assert wCH3.period + 1 - 2 == wCH3.lengthBit
or [hl]
or $80
ldh [rNR34], a
ret
FxTonePortaSetup:
runtime_assert c != $08, "Tone porta is not supported on CH4!"
; Setup portion: get the target period.
ld a, [de]
; Compute the target period from the note ID.
add a, a
add a, LOW(PeriodTable)
ld l, a
adc a, HIGH(PeriodTable)
sub l
ld h, a
ld a, [hli]
ld b, [hl]
; Write it.
ld hl, wCH1.portaTarget - wCH1.note
add hl, de
ld [hli], a
ld [hl], b
KnownRet:
ret
; This one is slightly out of order so it can `jr FxChangeTimbre2.chX`.
; For CH1 and CH2, this is written as-is to NRx1;
; for CH3, this is the ID of the wave to load;
; for CH4, this is the new LFSR width bit to write to NR43.
FxChangeTimbre:
; Don't touch the channel if not allowed to.
ldh a, [hUGE_AllowedChannels]
and c
ret z
; Dispatch.
rra
jr c, FxChangeTimbre2.ch1
rra
jr c, FxChangeTimbre2.ch2
rra
jr c, FxChangeTimbre2.ch3
.ch4
; Keep the polynom bits, but replace the LFSR width bit.
runtime_assert (b == 0) || (b == {AUD4POLY_7STEP}), "Invalid timbre change for CH4!"
ldh a, [rNR43]
and ~AUD4POLY_7STEP ; Reset the LFSR width bit.
or b
ldh [rNR43], a
ret
FxSetMasterVolume:
ld a, b ; Read the FX's params.
ldh [rNR50], a
ret
FxSetPanning:
ld a, b ; Read the FX's params.
ldh [FORTISSIMO_PANNING], a
ret
; FxChangeTimbre is a bit above.
FxPatternBreak:
ld a, b
ld [wForceRow], a
ret
FxVolumeSlide:
runtime_assert a != $04, "Volume slide is not supported for CH3!"
; Don't touch the channel if not allowed to.
ldh a, [hUGE_AllowedChannels]
and c
ret z
; Compute a pointer to the volume register.
; Doing it this way, courtesy of @nitro2k01, is smaller and faster.
; a = 1 for CH1, 2 for CH2, 8 for CH4.
xor 2 ; 3, 0, A
inc a ; 4, 1, B
xor 4 ; 0, 5, F
add a, LOW(rNR12)
ld c, a
; Prepare the FX params.
ld a, b
; $F0 appears quite a few times, cache it in a register.
ld b, $F0
and b ; "Up" part.
ld h, a ; "Up" stored in the *upper* byte.
ld a, b
swap a ; "Down" part.
and b
ld l, a ; "Down" part stored in the *lower* byte.
; Make volume adjustments.
ldh a, [c]
and b ; Keep only the current volume bits.
sub l
jr nc, :+
xor a ; Clamp.
:
add a, h
jr nc, :+
ld a, b ; Clamp.
:
; TODO: this only needs to apply when a = 0, which is equivalent to clamping... or Z on the `sub`.
or AUDENV_UP ; Ensure that writing $00 does *not* kill the channel (reduces pops).
.applyVolume
ldh [c], a ; Yes, this kills the envelope, but we must retrigger, which sounds bad with envelope anyway.
; Speaking of, retrigger the channel.
inc c ; LOW(rNRx3)
inc c ; LOW(rNRx4)
; CH4 doesn't have a period, so this'll read garbage;
; however, this is OK, because the lower 3 bits are ignored by the hardware register anyway.
; We only need to mask off the upper bits which might be set.
ld hl, wCH1.period + 1 - wCH1.note
add hl, de ; Go to the period's high byte
ldh a, [c]
xor [hl]
and AUDHIGH_LENGTH_ON | AUDHIGH_RESTART ; Preserve the length bit, and set the "restart" bit (always reads 1).
xor [hl]
ldh [c], a
ret
FxPosJump:
; Writing to `orderIdx` directly is safe, because it is only read by `ReadRow`,
; all calls to which happen before any FX processing. (The rows are cached in RAM.)
ld hl, wOrderIdx
ld a, b
ld [hli], a
; Set the necessary bits to make this non-zero;
; if a row is already being forced, this keeps it, but will select row 0 otherwise.
inc hl
assert wOrderIdx + 2 == wForceRow
assert LOW(-PATTERN_LENGTH) == $C0 ; Set the corresponding bits.
ld a, [hl]
or $C0
ld [hl], a
ret
; The jump table is in the middle of the functions so that backwards `jr`s can be used as well as forwards.
Tick0Fx:
To FxArpeggio, from Tick0Fx
No porta up
No porta down
To FxTonePortaSetup, from Tick0Fx
To FxResetVibCounter, from Tick0Fx
To FxSetMasterVolume, from Tick0Fx
To FxCallRoutine, from Tick0Fx
No note delay
To FxSetPanning, from Tick0Fx
To FxChangeTimbre, from Tick0Fx
To FxVolumeSlide, from Tick0Fx
To FxPosJump, from Tick0Fx
To FxSetVolume, from Tick0Fx
To FxPatternBreak, from Tick0Fx
To NoteCutTick0Trampoline, from Tick0Fx
FxSetSpeed:
ld a, b
ld [wTicksPerRow], a
; We want the new tempo to take effect immediately; so, we must reload the timer as well.
; This is easy to do, since we are on tick 0.
ld [wRowTimer], a
ret
FxSetVolume:
; Don't touch the channel if not allowed to.
ldh a, [hUGE_AllowedChannels]
and c
ret z
cp hUGE_CH3_MASK
jr z, .ch3
; Compute a pointer to the volume register.
; Doing it this way, courtesy of @nitro2k01, is smaller and faster.
; a = 1 for CH1, 2 for CH2, 8 for CH4.
xor 2 ; 3, 0, A