-
Notifications
You must be signed in to change notification settings - Fork 74
/
s2.sounddriver.asm
4104 lines (3626 loc) · 140 KB
/
s2.sounddriver.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
; Sonic the Hedgehog 2 disassembled Z80 sound driver
; Disassembled by Xenowhirl for AS
; Additional disassembly work by RAS Oct 2008
; RAS' work merged into SVN by Flamewing
; ---------------------------------------------------------------------------
FixDriverBugs = fixBugs
OptimiseDriver = 0
; ---------------------------------------------------------------------------
; NOTES:
;
; Set your editor's tab width to 8 characters wide for viewing this file.
;
; This code is compressed in the ROM, but you can edit it here as uncompressed
; and it will automatically be assembled and compressed into the correct place
; during the build process.
;
; This Z80 code can use labels and equates defined in the 68k code,
; and the 68k code can use the labels and equates defined in here.
; This is fortunate, as they contain references to each other's addresses.
;
; If you want to add significant amounts of extra code to this driver,
; try putting your code as far down as possible, after the function zDecEnd.
; That will make you less likely to run into space shortages from dislocated data alignment.
;
; >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Setup defines and macros
; zComRange: @ 1B80h
; +00h -- Priority of current SFX (cleared when 1-up song is playing)
; +01h -- tempo clock
; +02h -- current tempo
; +03h -- Pause/unpause flag: 7Fh for pause; 80h for unpause (set from 68K)
; +04h -- total volume levels to continue decreasing volume before fade out considered complete (starts at 28h, works downward)
; +05h -- delay ticker before next volume decrease
; +06h -- communication value
; +07h -- "DAC is updating" flag (set to FFh until completion of DAC track change)
; +08h -- When NOT set to 80h, 68K request new sound index to play
; +09h -- SFX to Play queue slot
; +0Ah -- Play stereo sound queue slot
; +0Bh -- Unknown SFX Queue slot
; +0Ch -- Address to table of voices
;
; +0Eh -- Set to 80h while fading in (disabling SFX) then 00h
; +0Fh -- Same idea as +05h, except for fade IN
; +10h -- Same idea as +04h, except for fade IN
; +11h -- 80h set indicating 1-up song is playing (stops other sounds)
; +12h -- main tempo value
; +13h -- original tempo for speed shoe restore
; +14h -- Speed shoes flag
; +15h -- If 80h, FM Channel 6 is NOT in use (DAC enabled)
; +16h -- value of which music bank to use (0 for MusicPoint1, $80 for MusicPoint2)
; +17h -- Pal mode flag
;
; ** zTracksSongStart starts @ +18h
;
; 1B98 base
; Track 1 = DAC
; Then 6 FM
; Then 3 PSG
;
;
; 1B98 = DAC
; 1BC2 = FM 1
; 1BEC = FM 2
; 1C16 = FM 3
; 1C40 = FM 4
; 1C6A = FM 5
; 1C94 = FM 6
; 1CBE = PSG 1
; 1CE8 = PSG 2
; 1D12 = PSG 3 (tone or noise)
;
; 1D3C = SFX FM 3
; 1D66 = SFX FM 4
; 1D90 = SFX FM 5
; 1DBA = SFX PSG 1
; 1DE4 = SFX PSG 2
; 1E0E = SFX PSG 3 (tone or noise)
;
;
zTrack STRUCT DOTS
; "playback control"; bits:
; 1 (02h): track is at rest
; 2 (04h): SFX is overriding this track
; 3 (08h): modulation on
; 4 (10h): do not attack next note
; 7 (80h): track is playing
PlaybackControl: ds.b 1
; "voice control"; bits:
; 2 (04h): If set, bound for part II, otherwise 0 (see zWriteFMIorII)
; -- bit 2 has to do with sending key on/off, which uses this differentiation bit directly
; 7 (80h): PSG track
VoiceControl: ds.b 1
TempoDivider: ds.b 1 ; Timing divisor; 1 = Normal, 2 = Half, 3 = Third...
DataPointerLow: ds.b 1 ; Track's position low byte
DataPointerHigh: ds.b 1 ; Track's position high byte
Transpose: ds.b 1 ; Transpose (from coord flag E9)
Volume: ds.b 1 ; Channel volume (only applied at voice changes)
AMSFMSPan: ds.b 1 ; Panning / AMS / FMS settings
VoiceIndex: ds.b 1 ; Current voice in use OR current PSG tone
VolFlutter: ds.b 1 ; PSG flutter (dynamically affects PSG volume for decay effects)
StackPointer: ds.b 1 ; "Gosub" stack position offset (starts at 2Ah, i.e. end of track, and each jump decrements by 2)
DurationTimeout: ds.b 1 ; Current duration timeout; counting down to zero
SavedDuration: ds.b 1 ; Last set duration (if a note follows a note, this is reapplied to 0Bh)
;
; ; 0Dh / 0Eh change a little depending on track -- essentially they hold data relevant to the next note to play
SavedDAC: ; DAC: Next drum to play
FreqLow: ds.b 1 ; FM/PSG: frequency low byte
FreqHigh: ds.b 1 ; FM/PSG: frequency high byte
NoteFillTimeout: ds.b 1 ; Currently set note fill; counts down to zero and then cuts off note
NoteFillMaster: ds.b 1 ; Reset value for current note fill
ModulationPtrLow: ds.b 1 ; Low byte of address of current modulation setting
ModulationPtrHigh: ds.b 1 ; High byte of address of current modulation setting
ModulationWait: ds.b 1 ; Wait for ww period of time before modulation starts
ModulationSpeed: ds.b 1 ; Modulation speed
ModulationDelta: ds.b 1 ; Modulation change per mod. Step
ModulationSteps: ds.b 1 ; Number of steps in modulation (divided by 2)
ModulationValLow: ds.b 1 ; Current modulation value low byte
ModulationValHigh: ds.b 1 ; Current modulation value high byte
Detune: ds.b 1 ; Set by detune coord flag E1; used to add directly to FM/PSG frequency
VolTLMask: ds.b 1 ; zVolTLMaskTbl value set during voice setting (value based on algorithm indexing zGain table)
PSGNoise: ds.b 1 ; PSG noise setting
VoicePtrLow: ds.b 1 ; Low byte of custom voice table (for SFX)
VoicePtrHigh: ds.b 1 ; High byte of custom voice table (for SFX)
TLPtrLow: ds.b 1 ; Low byte of where TL bytes of current voice begin (set during voice setting)
TLPtrHigh: ds.b 1 ; High byte of where TL bytes of current voice begin (set during voice setting)
LoopCounters: ds.b $A ; Loop counter index 0
; ... open ...
GoSubStack: ; start of next track, every two bytes below this is a coord flag "gosub" (F8h) return stack
;
; The bytes between +20h and +29h are "open"; starting at +20h and going up are possible loop counters
; (for coord flag F7) while +2Ah going down (never AT 2Ah though) are stacked return addresses going
; down after calling coord flag F8h. Of course, this does mean collisions are possible with either
; or other track memory if you're not careful with these! No range checking is performed!
;
; All tracks are 2Ah bytes long
zTrack ENDSTRUCT
zVar STRUCT DOTS
SFXPriorityVal: ds.b 1
TempoTimeout: ds.b 1
CurrentTempo: ds.b 1 ; Stores current tempo value here
StopMusic: ds.b 1 ; Set to 7Fh to pause music, set to 80h to unpause. Otherwise 00h
FadeOutCounter: ds.b 1
FadeOutDelay: ds.b 1
Communication: ds.b 1 ; Unused byte used to synchronise gameplay events with music
DACUpdating: ds.b 1 ; Set to FFh while DAC is updating, then back to 00h
QueueToPlay: ds.b 1 ; The head of the queue
Queue0: ds.b 1
Queue1: ds.b 1
Queue2: ds.b 1 ; This slot was totally broken in Sonic 1's driver. It's mostly fixed here, but it's still a little broken (see 'zInitMusicPlayback').
VoiceTblPtr: ds.b 2 ; Address of the voices
FadeInFlag: ds.b 1
FadeInDelay: ds.b 1
FadeInCounter: ds.b 1
1upPlaying: ds.b 1
TempoMod: ds.b 1
TempoTurbo: ds.b 1 ; Stores the tempo if speed shoes are acquired (or 7Bh is played otherwise)
SpeedUpFlag: ds.b 1
DACEnabled: ds.b 1
MusicBankNumber: ds.b 1
IsPalFlag: ds.b 1 ; Flags if the system is a PAL console
zVar ENDSTRUCT
; equates: standard (for Genesis games) addresses in the memory map
zYM2612_A0 = $4000
zYM2612_D0 = $4001
zYM2612_A1 = $4002
zYM2612_D1 = $4003
zBankRegister = $6000
zPSG = $7F11
zROMWindow = $8000
; More equates: addresses specific to this program (besides labelled addresses)
zMusicData = $1380 ; don't change this unless you change all the pointers in the BINCLUDE'd music too...
zStack = zMusicData+$800 ; 1B80h
phase zStack
zAbsVar: zVar
zTracksSongStart: ; This is the beginning of all BGM track memory
zSongDACFMStart:
zSongDAC: zTrack
zSongFMStart:
zSongFM1: zTrack
zSongFM2: zTrack
zSongFM3: zTrack
zSongFM4: zTrack
zSongFM5: zTrack
zSongFM6: zTrack
zSongFMEnd:
zSongDACFMEnd:
zSongPSGStart:
zSongPSG1: zTrack
zSongPSG2: zTrack
zSongPSG3: zTrack
zSongPSGEnd:
zTracksSongEnd:
zTracksSFXStart:
zSFX_FMStart:
zSFX_FM3: zTrack
zSFX_FM4: zTrack
zSFX_FM5: zTrack
zSFX_FMEnd:
zSFX_PSGStart:
zSFX_PSG1: zTrack
zSFX_PSG2: zTrack
zSFX_PSG3: zTrack
zSFX_PSGEnd:
zTracksSFXEnd:
zTracksSaveStart: ; When extra life plays, it backs up a large amount of memory (all track data plus 36 bytes)
zSaveVar: zVar
zSaveSongDAC: zTrack
zSaveSongFM1: zTrack
zSaveSongFM2: zTrack
zSaveSongFM3: zTrack
zSaveSongFM4: zTrack
zSaveSongFM5: zTrack
zSaveSongFM6: zTrack
zSaveSongPSG1: zTrack
zSaveSongPSG2: zTrack
zSaveSongPSG3: zTrack
zTracksSaveEnd:
; See the very end for another set of variables
if *>$2000
fatal "Z80 variables are \{*-$2000}h bytes past the end of Z80 RAM!"
endif
dephase
MUSIC_TRACK_COUNT = (zTracksSongEnd-zTracksSongStart)/zTrack.len
MUSIC_DAC_FM_TRACK_COUNT = (zSongDACFMEnd-zSongDACFMStart)/zTrack.len
MUSIC_FM_TRACK_COUNT = (zSongFMEnd-zSongFMStart)/zTrack.len
MUSIC_PSG_TRACK_COUNT = (zSongPSGEnd-zSongPSGStart)/zTrack.len
SFX_TRACK_COUNT = (zTracksSFXEnd-zTracksSFXStart)/zTrack.len
SFX_FM_TRACK_COUNT = (zSFX_FMEnd-zSFX_FMStart)/zTrack.len
SFX_PSG_TRACK_COUNT = (zSFX_PSGEnd-zSFX_PSGStart)/zTrack.len
; In what I believe is an unfortunate design choice in AS,
; both the phased and unphased PCs must be within the target processor's range,
; which means phase is useless here despite being designed to fix this problem...
; oh well, I set it up to fix this later when processing the .p file
!org 0 ; Z80 code starting at address 0 has special meaning to s2p2bin.exe
CPU Z80UNDOC
listing purecode
; Macro to perform a bank switch... after using this,
; the start of zROMWindow points to the start of the given 68k address,
; rounded down to the nearest $8000 byte boundary
bankswitch macro addr68k
if OptimiseDriver
; Because why use a and e when you can use h and l?
ld hl,zBankRegister+1 ; +1 so that 6000h becomes 6001h, which is still a valid bankswitch port
.cnt := 0
rept 9
; this is either ld (hl),h or ld (hl),l
db 74h|(((addr68k)&(1<<(15+.cnt)))<>0)
.cnt := .cnt+1
endm
else
xor a ; a = 0
ld e,1 ; e = 1
ld hl,zBankRegister
.cnt := 0
rept 9
; this is either ld (hl),a or ld (hl),e
db 73h|((((addr68k)&(1<<(15+.cnt)))=0)<<2)
.cnt := .cnt+1
endm
endif
endm
; macro to make a certain error message clearer should you happen to get it...
rsttarget macro {INTLABEL}
if ($&7)||($>38h)
fatal "Function __LABEL__ is at 0\{$}h, but must be at a multiple of 8 bytes <= 38h to be used with the rst instruction."
endif
if "__LABEL__"<>""
__LABEL__ label $
endif
endm
; function to decide whether an offset's full range won't fit in one byte
offsetover1byte function from,maxsize, ((from&0FFh)>(100h-maxsize))
; macro to make sure that ($ & 0FF00h) == (($+maxsize) & 0FF00h)
ensure1byteoffset macro maxsize
if offsetover1byte($,maxsize)
startpad := $
align 100h
if MOMPASS=1
endpad := $
if endpad-startpad>=1h
; warn because otherwise you'd have no clue why you're running out of space so fast
warning "had to insert \{endpad-startpad}h bytes of padding before improperly located data at 0\{startpad}h in Z80 code"
endif
endif
endif
endm
; Function to turn a 68k address into a word the Z80 can use to access it,
; assuming the correct bank has been switched to first
zmake68kPtr function addr,zROMWindow+(addr&7FFFh)
; Function to turn a sample rate into a djnz loop counter
pcmLoopCounterBase function sampleRate,baseCycles, 1+(Z80_Clock/(sampleRate)-(baseCycles)+(13/2))/13
pcmLoopCounter function sampleRate, pcmLoopCounterBase(sampleRate,146/2) ; 146 is the number of cycles zPlaySegaSound takes to deliver two samples.
dpcmLoopCounter function sampleRate, pcmLoopCounterBase(sampleRate,289/2) ; 289 is the number of cycles zWriteToDAC takes to deliver two samples.
; >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Z80 'ROM' start:
; zEntryPoint:
di ; Disable interrupts
ld sp,zStack
jp zStartDAC
; ---------------------------------------------------------------------------
; zbyte_7:
zPalModeByte:
db 0
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
if ~~OptimiseDriver ; This is redundant: the Z80 is slow enough to not need to worry about this
align 8
; zsub_8
zFMBusyWait: rsttarget
; Performs the annoying task of waiting for the FM to not be busy
ld a,(zYM2612_A0)
add a,a
jr c,zFMBusyWait
ret
; End of function zFMBusyWait
endif
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
align 8
; zsub_10
zWriteFMIorII: rsttarget
bit 2,(ix+zTrack.VoiceControl)
jr z,zWriteFMI
jr zWriteFMII
; End of function zWriteFMIorII
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
align 8
; zsub_18
zWriteFMI: rsttarget
; Write reg/data pair to part I; 'a' is register, 'c' is data
if ~~OptimiseDriver
push af
rst zFMBusyWait ; 'rst' is like 'call' but only works for 8-byte aligned addresses <= 38h
pop af
endif
ld (zYM2612_A0),a
push af
if ~~OptimiseDriver
rst zFMBusyWait
endif
ld a,c
ld (zYM2612_D0),a
pop af
ret
; End of function zWriteFMI
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
align 8
; zsub_28
zWriteFMII: rsttarget
; Write reg/data pair to part II; 'a' is register, 'c' is data
if ~~OptimiseDriver
push af
rst zFMBusyWait
pop af
endif
ld (zYM2612_A1),a
push af
if ~~OptimiseDriver
rst zFMBusyWait
endif
ld a,c
ld (zYM2612_D1),a
pop af
ret
; End of function zWriteFMII
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
org 38h
zVInt: rsttarget
; This is called every VBLANK (38h is the interrupt entry point,
; and VBLANK is the only one Z80 is hooked up to.)
push af ; Save 'af'
exx ; Effectively backs up 'bc', 'de', and 'hl'
call zBankSwitchToMusic ; Bank switch to the music
xor a ; Clear 'a'
ld (zDoSFXFlag),a ; Not updating SFX (updating music)
ld ix,zAbsVar ; ix points to zComRange
ld a,(zAbsVar.StopMusic) ; Get pause/unpause flag
or a ; Test 'a'
jr z,zUpdateEverything ; If zero, go to zUpdateEverything
call zPauseMusic
jp zUpdateDAC ; Now update the DAC
; ---------------------------------------------------------------------------
; zloc_51
zUpdateEverything:
ld a,(zAbsVar.FadeOutCounter) ; Are we fading out?
or a
call nz,zUpdateFadeout ; If so, update that
ld a,(zAbsVar.FadeInFlag) ; Are we fading in?
or a
call nz,zUpdateFadeIn ; If so, update that
ld a,(zAbsVar.Queue0)
or (ix+zVar.Queue1)
or (ix+zVar.Queue2) ; This was missing in Sonic 1's driver, breaking the third queue slot.
call nz,zCycleQueue ; If any of those are non-zero, cycle queue
; Apparently if this is 80h, it does not play anything new,
; otherwise it cues up the next play (flag from 68K for new item)
ld a,(zAbsVar.QueueToPlay)
cp 80h
call nz,zPlaySoundByIndex ; If not 80h, we need to play something new!
; Spindash update
ld a,(zSpindashPlayingCounter)
or a
jr z,.pal_timer ; If the spindash counter is already 0, jump
dec a ; Decrease the spindash sound playing counter
ld (zSpindashPlayingCounter),a
.pal_timer:
; If the system is PAL, then this performs some timing adjustments
; (i.e. you need to update 1.2x as much to keep up the same rate)
ld hl,zPalModeByte ; Get address of zPalModeByte
ld a,(zAbsVar.IsPalFlag) ; Get IsPalFlag -> 'a'
and (hl) ; 'And' them together
jr z,.not_pal ; If it comes out zero, do nothing
ld hl,zPALUpdTick
dec (hl)
jr nz,.not_pal
ld (hl),5 ; Every 6 frames (0-5) you need to "double update" to sort of keep up
call zUpdateMusic
.not_pal:
call zUpdateMusic
; Now all of the SFX tracks are updated in a similar manner to "zUpdateMusic"...
bankswitch SoundIndex ; Bank switch to sound effects
ld a,80h
ld (zDoSFXFlag),a ; Set zDoSFXFlag = 80h (updating sound effects)
if FixDriverBugs
; A bugfix in zUpdateMusic prevents it from returning ix set to a convenient value, so set it explicitly here
ld ix,zTracksSFXStart-zTrack.len
endif
; FM SFX channels
ld b,SFX_FM_TRACK_COUNT ; Only 3 FM channels for SFX (FM3, FM4, FM5)
.fmloop:
push bc
ld de,zTrack.len ; Spacing between tracks
add ix,de ; Next track
bit 7,(ix+zTrack.PlaybackControl) ; Is it playing?
call nz,zFMUpdateTrack ; If it is, go update it
pop bc
djnz .fmloop
; PSG SFX channels
ld b,SFX_PSG_TRACK_COUNT ; All PSG channels available
.psgloop:
push bc
ld de,zTrack.len ; Spacing between tracks
add ix,de ; Next track
bit 7,(ix+zTrack.PlaybackControl) ; Is it playing?
call nz,zPSGUpdateTrack ; If it is, go update it
pop bc
djnz .psgloop
; Now we update the DAC... this only does anything if there's a new DAC
; sound to be played. This is called after updating the DAC track.
; Otherwise it just mucks with the timing loop, forcing an update.
zUpdateDAC:
bankswitch SndDAC_Start ; Bankswitch to the DAC data
ld a,(zCurDAC) ; Get currently playing DAC sound
or a
jp m,.dacqueued ; If one is queued (80h+), go to it!
exx ; Otherwise restore registers from mirror regs
ld b,1 ; b=1 (initial feed to the DAC "djnz" loops, i.e. UPDATE RIGHT AWAY)
pop af
ei ; Enable interrupts
ret
.dacqueued:
; If you get here, it's time to start a new DAC sound...
ld a,80h
ex af,af' ;'
ld a,(zCurDAC) ; Get current DAC sound
sub 81h ; Subtract 81h (first DAC index is 81h)
ld (zCurDAC),a ; Store that as current DAC sound
; The following two instructions are dangerous: they discard the upper
; two bits of zCurDAC, meaning you can only have 40h DAC samples.
add a,a
add a,a ; a *= 4 (each DAC entry is a pointer and length, 2+2)
add a,zDACPtrTbl&0FFh ; Get low byte into table -> 'a'
ld (zDACStartAddress+1),a ; Store into the instruction after zDACStartAddress (self-modifying code)
add a,zDACLenTbl-zDACPtrTbl ; How to offset to length versus pointer
ld (zDACStoreLength+2),a ; Store into the instruction after zDACStoreLength (self-modifying code)
pop af
ld hl,zWriteToDAC
ex (sp),hl ; Jump to zWriteToDAC
; zloc_104
zDACStartAddress:
ld hl,(zDACPtrTbl) ; Self-modified code: sets start address of DAC sample for zWriteToDAC
; zloc_107
zDACStoreLength:
ld de,(zDACLenTbl) ; Self-modified code: sets length of DAC sample for zWriteToDAC
;zloc_10B
zDACStoreDelay:
ld bc,100h ; Self-modified code: From zDACDataStore; sets b=1 (the 100h part of it) UPDATE RIGHT AWAY and c="data rate delay" for this DAC sample, the future 'b' setting
ei ; Enable interrupts
ret
; End of function zVInt
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; Updates all tracks; queues notes and durations!
; zsub_110
zUpdateMusic:
if ~~FixDriverBugs
; Calling this function here is bad, because it can cause the
; first note of a newly-started song to be delayed for a frame.
; The vanilla driver resorts to a workaround to prevent such a
; delay from having side-effects, but it's better to just fix
; the problem directly, and move this call to the proper place.
call TempoWait
endif
; DAC updates
ld a,0FFh
ld (zAbsVar.DACUpdating),a ; Store FFh to DACUpdating
ld ix,zTracksSongStart ; Point "ix" to zTracksSongStart
bit 7,(ix+zTrack.PlaybackControl) ; Is bit 7 (80h) set on playback control byte? (means "is playing")
call nz,zDACUpdateTrack ; If so, zDACUpdateTrack
xor a ; Clear a
ld (zAbsVar.DACUpdating),a ; Store 0 to DACUpdating
ld b,MUSIC_FM_TRACK_COUNT ; Loop 6 times (FM)...
.fmloop:
push bc
ld de,zTrack.len ; Space between tracks
add ix,de ; Go to next track
bit 7,(ix+zTrack.PlaybackControl) ; Is bit 7 (80h) set on playback control byte? (means "is playing")
call nz,zFMUpdateTrack ; If so...
pop bc
djnz .fmloop
ld b,MUSIC_PSG_TRACK_COUNT ; Loop 3 times (PSG)...
.psgloop:
push bc
ld de,zTrack.len ; Space between tracks
add ix,de ; Go to next track
bit 7,(ix+zTrack.PlaybackControl) ; Is this track playing?
call nz,zPSGUpdateTrack ; If so...
pop bc
djnz .psgloop
if ~~FixDriverBugs
; See above. Removing this instruction will cause this
; subroutine to fall-through to TempoWait.
ret
endif
; End of function zUpdateMusic
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; zsub_14C
TempoWait:
; Tempo works as divisions of the 60Hz clock (there is a fix supplied for
; PAL that "kind of" keeps it on track.) Every time the internal music clock
; overflows, it will update. So a tempo of 80h will update every other
; frame, or 30 times a second.
ld ix,zAbsVar ; ix points to zComRange
ld a,(ix+zVar.CurrentTempo) ; tempo value
add a,(ix+zVar.TempoTimeout) ; Adds previous value to
ld (ix+zVar.TempoTimeout),a ; Store this as new
ret c ; If addition overflowed (answer greater than FFh), return
; So if adding tempo value did NOT overflow, then we add 1 to all durations
ld hl,zTracksSongStart+zTrack.DurationTimeout ; Start at first track's delay counter (counting up to delay)
ld de,zTrack.len ; Offset between tracks
ld b,MUSIC_TRACK_COUNT ; Loop for all tracks
.tempoloop:
inc (hl) ; Increasing delay tick counter to target
add hl,de ; Next track...
djnz .tempoloop
ret
; End of function TempoWait
; ---------------------------------------------------------------------------
; zloc_167
zStartDAC:
im 1 ; set interrupt mode 1
call zClearTrackPlaybackMem
ei ; enable interrupts
ld iy,zDACDecodeTbl
ld de,0
; This controls the update rate for the DAC...
; My speculation for the rate at which the DAC updates:
; Z80 clock = 3.57954MHz = 3579540Hz (not sure?)
; zWaitLoop (immediately below) is waiting for someone to set
; 'de' (length of DAC sample) to non-zero
; The DAC code is sync'ed with VBLANK somehow, but I haven't quite got that
; one figure out yet ... tests on emulator seem to confirm it though.
; Next, there are "djnz" loops which do busy-waits. (Two of them, which is
; where the '2' comes from in my guess equation.) The DACMasterPlaylist
; appears to use '1' as the lowest input value to these functions. (Which,
; given the djnz, would be the fastest possible value.)
; The rate seems to be calculated by 3579540Hz / (60Hz + (speed * 4)) / 2
; This "waitLoop" waits if the DAC has no data, or else it drops out!
; zloc_174:
zWaitLoop:
ld a,d ; 4
or e ; 4
jr z,zWaitLoop ; 7 ; As long as 'de' (length of sample) = 0, wait...
; 'hl' is the pointer to the sample, 'de' is the length of the sample,
; and 'iy' points to the translation table; let's go...
; The "djnz $" loops control the playback rate of the DAC
; (the higher the 'b' value, the slower it will play)
; As for the actual encoding of the data, it is described by jman2050:
; "As for how the data is compressed, lemme explain that real quick:
; First, it is a lossy compression. So if you recompress a PCM sample this way,
; you will lose precision in data. Anyway, what happens is that each compressed data
; is separated into nybbles (1 4-bit section of a byte). This first nybble of data is
; read, and used as an index to a table containing the following data:
; 0,1,2,4,8,$10,$20,$40,$80,$FF,$FE,$FC,$F8,$F0,$E0,$C0." [zDACDecodeTbl / zbyte_1B3]
; "So if the nybble were equal to F, it'd extract $C0 from the table. If it were 8,
; it would extract $80 from the table. ... Anyway, there is also another byte of data
; that we'll call 'd'. At the start of decompression, d is $80. What happens is that d
; is then added to the data extracted from the table using the nybble. So if the nybble
; were 4, the 8 would be extracted from the table, then added to d, which is $80,
; resulting in $88. This result is then put back into d, then fed into the YM2612 for
; processing. Then the next nybble is read, the data is extracted from the table, then
; is added to d (remember, d is now changed because of the previous operation), then is
; put back into d, then is fed into the YM2612. This process is repeated until the number
; of bytes as defined in the table above are read and decompressed."
; In our case, the so-called 'd' value is shadow register 'a'
zWriteToDAC:
djnz $ ; 8 ; Busy wait for specific amount of time in 'b'
di ; 4 ; disable interrupts (while updating DAC)
ld a,2Ah ; 7 ; DAC port
ld (zYM2612_A0),a ; 13 ; Set DAC port register
ld a,(hl) ; 7 ; Get next DAC byte
rlca ; 4
rlca ; 4
rlca ; 4
rlca ; 4
and 0Fh ; 7 ; UPPER 4-bit offset into zDACDecodeTbl
ld (.highnybble+2),a ; 13 ; store into the instruction after .highnybble (self-modifying code)
ex af,af' ; 4 ; shadow register 'a' is the 'd' value for 'jman2050' encoding
; zloc_18B
.highnybble:
add a,(iy+0) ; 19 ; Get byte from zDACDecodeTbl (self-modified to proper index)
ld (zYM2612_D0),a ; 13 ; Write this byte to the DAC
ex af,af' ; 4 ; back to regular registers
ld b,c ; 4 ; reload 'b' with wait value
ei ; 4 ; enable interrupts (done updating DAC, busy waiting for next update)
djnz $ ; 8 ; Busy wait for specific amount of time in 'b'
di ; 4 ; disable interrupts (while updating DAC)
push af ; 11
pop af ; 11
ld a,2Ah ; 7 ; DAC port
ld (zYM2612_A0),a ; 13 ; Set DAC port register
ld b,c ; 4 ; reload 'b' with wait value
ld a,(hl) ; 7 ; Get next DAC byte
inc hl ; 6 ; Next byte in DAC stream...
dec de ; 6 ; One less byte
and 0Fh ; 7 ; LOWER 4-bit offset into zDACDecodeTbl
ld (.lownybble+2),a ; 13 ; store into the instruction after .lownybble (self-modifying code)
ex af,af' ; 4 ; shadow register 'a' is the 'd' value for 'jman2050' encoding
; zloc_1A8
.lownybble:
add a,(iy+0) ; 19 ; Get byte from zDACDecodeTbl (self-modified to proper index)
ld (zYM2612_D0),a ; 13 ; Write this byte to the DAC
ex af,af' ; 4 ; back to regular registers
ei ; 4 ; enable interrupts (done updating DAC, busy waiting for next update)
jp zWaitLoop ; 10 ; Back to the wait loop; if there's more DAC to write, we come back down again!
; 289
; 289 cycles for two samples. dpcmLoopCounter should use 289 divided by 2.
; ---------------------------------------------------------------------------
; 'jman2050' DAC decode lookup table
; zbyte_1B3
zDACDecodeTbl:
db 0, 1, 2, 4, 8, 10h, 20h, 40h
db 80h, -1, -2, -4, -8, -10h, -20h, -40h
; The following two tables are used for when an SFX terminates
; its track to properly restore the music track it temporarily took
; over. Note that an important rule here is that no SFX may use
; DAC, FM Channel 1, FM Channel 2, or FM Channel 6, period.
; Thus there's also only SFX tracks starting at FM Channel 3.
; The zeroes appear after FM 3 because it calculates the offsets into
; these tables by their channel assignment, where between Channel 3
; and Channel 4 there is a gap numerically.
ensure1byteoffset 10h
; zbyte_1C3
zMusicTrackOffs:
; These are offsets to different music tracks starting with FM3
dw zSongFM3, 0000h, zSongFM4, zSongFM5 ; FM3, 0, FM4, FM5
dw zSongPSG1, zSongPSG2, zSongPSG3, zSongPSG3 ; PSG1, PSG2, PSG3, PSG3 (noise alternate)
ensure1byteoffset 10h
; zbyte_1D3
zSFXTrackOffs:
; These are offsets to different sound effect tracks starting with FM3
dw zSFX_FM3, 0000h, zSFX_FM4, zSFX_FM5 ; FM3, 0, FM4, FM5
dw zSFX_PSG1, zSFX_PSG2, zSFX_PSG3, zSFX_PSG3 ; PSG1, PSG2, PSG3, PSG3 (noise alternate)
; ---------------------------------------------------------------------------
zDACUpdateTrack:
dec (ix+zTrack.DurationTimeout)
ret nz
ld l,(ix+zTrack.DataPointerLow)
ld h,(ix+zTrack.DataPointerHigh)
.sampleloop:
ld a,(hl) ; Get next byte from DAC Track
inc hl ; Move to next position...
cp 0E0h ; Check if is coordination flag
jr c,.notcoord ; Not coord flag? Skip ahead
call zCoordFlag ; Handle coordination flag
jp .sampleloop ; Loop back around...
.notcoord:
or a ; Test 'a' for 80h not set, which is a note duration
jp p,.gotduration ; If note duration, jump ahead (note that "hl" is already incremented)
ld (ix+zTrack.SavedDAC),a ; This is a note; store it here
ld a,(hl) ; Get next byte...
or a ; Test 'a' for 80h not set, which is a note duration
jp p,.repeatduration ; Is this a duration this time?? If so, jump ahead (only difference is to increment "hl")
; Note followed by a note... apparently recycles the previous duration
ld a,(ix+zTrack.SavedDuration) ; Current DAC note ticker goal value -> 'a'
ld (ix+zTrack.DurationTimeout),a ; Use it again
jr zDACAfterDur ; Jump to after duration subroutine...
; ---------------------------------------------------------------------------
; zloc_20D
.repeatduration:
inc hl ; Goes to next byte (after duration byte)
; zloc_20E
.gotduration:
call zSetDuration
; zloc_211
zDACAfterDur:
ld (ix+zTrack.DataPointerLow),l ; Stores "hl" to the DAC track pointer memory
ld (ix+zTrack.DataPointerHigh),h
bit 2,(ix+zTrack.PlaybackControl) ; Is SFX overriding this track?
ret nz ; If so, we're done
ld a,(ix+zTrack.SavedDAC) ; Check next note to play
cp 80h ; Is it a rest?
ret z ; If so, quit
sub 81h ; Otherwise, transform note into an index... (we're selecting which drum to play!)
add a,a ; Multiply by 2...
add a,zDACMasterPlaylist&0FFh ; Offset into list
ld (zDACDataStore+2),a ; store into the following instruction (self-modifying code)
; zloc_22A
zDACDataStore:
ld bc,(zDACMasterPlaylist) ; Load appropriate drum info -> bc
ld a,c ; DAC sample number (81h base) -> 'a'
ld (zCurDAC),a ; Store current DAC sound to play
ld a,b ; Data rate delay -> 'b'
ld (zDACStoreDelay+1),a ; store into the instruction after zDACStoreDelay (self-modifying code)
ret
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; zsub_237
zFMUpdateTrack:
dec (ix+zTrack.DurationTimeout) ; Decrement duration
jr nz,.notegoing ; If not time-out yet, go do updates only
res 4,(ix+zTrack.PlaybackControl) ; When duration over, clear "do not attack" bit 4 (0x10) of track's play control
call zFMDoNext ; Handle coordination flags, get next note and duration
call zFMPrepareNote ; Prepares to play next note
call zFMNoteOn ; Actually key it (if allowed)
call zDoModulation ; Update modulation (if modulation doesn't change, we do not return here)
jp zFMUpdateFreq ; Applies frequency update from modulation
.notegoing:
call zNoteFillUpdate ; Applies "note fill" (time until cut-off); NOTE: Will not return here if "note fill" expires
call zDoModulation ; Update modulation (if modulation doesn't change, we do not return here)
jp zFMUpdateFreq ; Applies frequency update from modulation
; End of function zFMUpdateTrack
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; zsub_258
zFMDoNext:
ld l,(ix+zTrack.DataPointerLow) ; Load track position low byte
ld h,(ix+zTrack.DataPointerHigh) ; Load track position high byte
res 1,(ix+zTrack.PlaybackControl) ; Clear bit 1 (02h) "track is rest" from track
.noteloop:
ld a,(hl)
inc hl ; Increment track to next byte
cp 0E0h ; Is it a control byte / "coordination flag"?
jr c,.gotnote ; If not, jump over
call zCoordFlag ; Handle coordination flag
jr .noteloop ; Go around, get next byte
.gotnote:
push af
call zFMNoteOff ; Send key off
pop af
or a ; Test 'a' for 80h not set, which is a note duration
jp p,.gotduration ; If duration, jump to .gotduration
call zFMSetFreq ; Otherwise, this is a note; call zFMSetFreq
ld a,(hl) ; Get next byte
or a ; Test 'a' for 80h set, which is a note
jp m,zFinishTrackUpdate ; If this is a note, jump to zFinishTrackUpdate
inc hl ; Otherwise, go to next byte; a duration
.gotduration:
call zSetDuration
jp zFinishTrackUpdate ; Either way, jumping to zFinishTrackUpdate...
; End of function zFMDoNext
; ---------------------------------------------------------------------------
; zloc_285 zGetFrequency
zFMSetFreq:
; 'a' holds a note to get frequency for
sub 80h
jr z,zFMDoRest ; If this is a rest, jump to zFMDoRest
add a,(ix+zTrack.Transpose) ; Add current channel transpose (coord flag E9)
add a,a ; Offset into Frequency table...
if OptimiseDriver
ld d,12*2 ; 12 notes per octave
ld c,0 ; Clear c (will hold octave bits)
.loop:
sub d ; Subtract 1 octave from the note
jr c,.getoctave ; If this is less than zero, we are done
inc c ; One octave up
jr .loop
.getoctave:
add a,d ; Add 1 octave back (so note index is positive)
sla c
sla c
sla c ; Multiply octave value by 8, to get final octave bits
endif
add a,zFrequencies&0FFh
ld (.storefreq+2),a ; Store into the instruction after .storefreq (self-modifying code)
; ld d,a
; adc a,(zFrequencies&0FF00h)>>8
; sub d
; ld (.storefreq+3),a ; This is how you could store the high byte of the pointer too (unnecessary if it's in the right range)
; zloc_292
.storefreq:
ld de,(zFrequencies) ; Stores frequency into "de"
ld (ix+zTrack.FreqLow),e ; Frequency low byte -> trackPtr + 0Dh
if OptimiseDriver
ld a,d
or c
ld (ix+zTrack.FreqHigh),a ; Frequency high byte -> trackPtr + 0Eh
else
ld (ix+zTrack.FreqHigh),d ; Frequency high byte -> trackPtr + 0Eh
endif
ret
; ---------------------------------------------------------------------------
; zloc_29D
zFMDoRest:
set 1,(ix+zTrack.PlaybackControl) ; Set bit 1 (track is at rest)
xor a ; Clear 'a'
ld (ix+zTrack.FreqLow),a ; Zero out FM Frequency
ld (ix+zTrack.FreqHigh),a ; Zero out FM Frequency
ret
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; zsub_2A9
zSetDuration:
ld c,a ; 'a' = current duration
ld b,(ix+zTrack.TempoDivider) ; Divisor; causes multiplication of duration for every number higher than 1
.multloop:
djnz .multloop_outer
ld (ix+zTrack.SavedDuration),a ; Store new duration into ticker goal of this track (this is reused if a note follows a note without a new duration)
ld (ix+zTrack.DurationTimeout),a ; Sets it on ticker (counts to zero)
ret
.multloop_outer:
add a,c ; Will multiply duration based on 'b'
jp .multloop
; End of function zSetDuration
; ---------------------------------------------------------------------------
; zloc_2BA
zFinishTrackUpdate:
; Common finish-up routine used by FM or PSG
ld (ix+zTrack.DataPointerLow),l ; Stores "hl" to the track pointer memory
ld (ix+zTrack.DataPointerHigh),h
ld a,(ix+zTrack.SavedDuration) ; Last set duration
ld (ix+zTrack.DurationTimeout),a ; ... put into ticker
bit 4,(ix+zTrack.PlaybackControl) ; Is bit 4 (10h) "do not attack next note" set on playback?
ret nz ; If so, quit
ld a,(ix+zTrack.NoteFillMaster) ; Master "note fill" value -> a
ld (ix+zTrack.NoteFillTimeout),a ; Reset 0Fh "note fill" value to master
ld (ix+zTrack.VolFlutter),0 ; Reset PSG flutter byte
bit 3,(ix+zTrack.PlaybackControl) ; is modulation turned on?
ret z ; if not, quit
ld l,(ix+zTrack.ModulationPtrLow) ; Otherwise, get address of modulation setting
ld h,(ix+zTrack.ModulationPtrHigh)
jp zSetModulation ; ... and go do it!
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; zsub_2E3
zNoteFillUpdate:
ld a,(ix+zTrack.NoteFillTimeout) ; Get current note fill value
or a
ret z ; If zero, return!
dec (ix+zTrack.NoteFillTimeout) ; Decrement note fill
ret nz ; If not zero, return
set 1,(ix+zTrack.PlaybackControl) ; Set bit 1 (track is at rest)
pop de ; return address -> 'de' (will not return to zUpdateTrack function!!)
bit 7,(ix+zTrack.VoiceControl) ; Is this a PSG track?
jp nz,zPSGNoteOff ; If so, jump to zPSGNoteOff
jp zFMNoteOff ; Else, jump to zFMNoteOff
; End of function zNoteFillUpdate
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; zsub_2FB
zDoModulation:
pop de ; keep return address -> de (MAY not return to caller)
bit 1,(ix+zTrack.PlaybackControl) ; Is "track in rest"?
ret nz ; If so, quit
bit 3,(ix+zTrack.PlaybackControl) ; Is modulation on?
ret z ; If not, quit
ld a,(ix+zTrack.ModulationWait) ; 'ww' period of time before modulation starts
or a
jr z,.waitdone ; if zero, go to it!
dec (ix+zTrack.ModulationWait) ; Otherwise, decrement timer
ret ; return if decremented (does NOT return to zUpdateTrack!!)
.waitdone:
dec (ix+zTrack.ModulationSpeed) ; Decrement modulation speed counter
ret nz ; Return if not yet zero