-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsidobj64.asm
More file actions
1909 lines (1768 loc) · 92.8 KB
/
Copy pathsidobj64.asm
File metadata and controls
1909 lines (1768 loc) · 92.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
.const PLAY_DEMO_SONG = false
/*
Enhanced SID Player by Craig Chamberlain / 1986
Disassembled by Chris Zinn / October - November 2025
This is in Kick Assembler format: https://theweb.dk/KickAssembler
Notes:
Uses the CIA Timer 1 / Plays at the same speed in NTSC and PAL
Default Memory Layout: $C000 - $CC51 49152 - 52305 (3,153 bytes)
Change PLAYER_ADDRESS to assemble it anywhere you like. ($C000 is the default)
Change SONG_ADDRESS to load a test song anywhere you like ($8000 is the default)
Change PLAY_DEMO_SONG = true to try it out with a demo song.
How to use the Player in your own code:
---------------------------------------
Load the song into memory
jsr INSTALL_SID_PLAYER to install the IRQ Handler
jsr INIT_SONG with the address of the song in x,y (lo,hi)
optionally store a address to a routine at AuxRoutineAddrLoHi to have the AUX command call your code.
Set SID_STATUS to %00000111 to start playing all voices (Bits 0,1,2 for voices 1,2,3)
Read FLAG_STATUS to get last value used by a FLG Command.
Stopping a song that is playing:
lda #$00
sta SID_STATUS
jsr HUSH_PLAYER
jsr REMOVE_SID_PLAYER to restore the IRQ Handler to what is before INSTALL_SID_PLAYER was installed.
ZERO Page Usage
Preserved on each interrupt: FB through FF. $FD,$FE Address of the next command on the current voice
Used but not preserved: (In the INIT_SONG routine) : $61 through $65 (Floating Point Accumulator #1 used by BASIC).
*/
.cpu _6502NoIllegals
.if (PLAY_DEMO_SONG == true)
{
BasicUpstart2(testPlayer)
testPlayer: jsr INSTALL_SID_PLAYER
ldx #<SONG_ADDRESS
ldy #>SONG_ADDRESS
jsr INIT_SONG
lda #%00000111 // All 3 voices go
sta SID_STATUS
rts // Exit to basic with the song playing
.label SONG_ADDRESS = $8000
* = SONG_ADDRESS "Test Song"
.import c64 "music/commodore.mus" // Import C64 PRG file without the 2 byte program header
.label SONG_ADDRESS_END = *
.print "Song Start Address : $" + toHexString(SONG_ADDRESS,4) + " " + SONG_ADDRESS
.print "Song End Address : $" + toHexString(SONG_ADDRESS_END,4) + " " + SONG_ADDRESS_END
.print "Song Size : " + (SONG_ADDRESS_END - SONG_ADDRESS) + " bytes"
}
.namespace SID
{
.label voice1Control = $d404
.label voice2Control = $d40b
.label voice3Control = $d412
.label voice1FrequencyHi = $d401
.label voice2FrequencyHi = $d408
.label voice3FrequencyHi = $d40f
.label filterCutoffLo = $d415
.label filterCutoffHi = $d416
.label filterResonanceControl = $d417
.label filterVolumeAndFilter = $d418
}
.label PLAYER_ADDRESS = $c000
* = PLAYER_ADDRESS "Working Data"
SID_STATUS: .byte 0 // bit 0-2 Enable/Disable voice
// Bit 3 Illegal state exception (The IRQ started, but bit 7 was on)
// Bit 34 OtherError (Seems to be related to IllegalNoteDuration)
// Bit 4 Illegal Note Duration (Example: 16th. 32nd. in default tempo for example)
// Bit 45 Illegal Phrase Error (Calling a phrase that doesn't exist)
// Bit 35 Stack Overflow Error (Calling a phrase from a phrase more than 5 times)
// Bit 345 Calling a phrase from another voice / Repeat called but the phrase definition was on a different voice
// Bit 5 END reached, but no DEF
// Bit 6 ?
// Bit 7 IRQ In progress. This makes sure that the IRQ Cannot run on top of itself while the IRQ is already running
FLAG_STATUS: .byte 0 // START OF WORK MEMORY
WorkTypeOctNote: .byte 0,0,0
WorkNoteDuration: .byte 0,0,0
WorkMeasureLo: .byte 0,0,0
WorkMeasureHi: .byte 0,0,0
WorkTargetNoteFreqLo: .byte 0,0,0 // Frequency Portamento is sliding towards (Command: POR)
WorkTargetNoteFreqHi: .byte 0,0,0
WorkPulseWidth2Lo: .byte 0,0,0
WorkPulseWidth2Hi: .byte 0,0,0
WorkVc1Control: .byte 0 // These mirror the bits in $D404 SID.voice1Control
WorkVc2Control: .byte 0 // These mirror the bits in $D40B SID.voice2Control
WorkVc3Control: .byte 0 // These mirror the bits in $D412 SID.voice3Control
WorkAttackDecays: .byte 0,0,0
WorkSusRelease: .byte 0,0,0
WorkFilterSweepState: .byte 0
WorkFilterCutOff: .byte 0
WorkFilterControl: .byte 0
WorkVolAndFilter: .byte 0
WorkTempo: .byte 0 // $90 default values in play routine. See CommandTem,
WorkTempoTableValue: .byte 0 // $60 See CommandTem, this is the va;ie looked up value from DataTempoTable for $90
WorkUTLValue: .byte 0 // $0c
WorkUTVValues: .byte 0,0,0
WorkCiaTimerA: .byte 0,0 // Lo/Hi of adjusted CIA Timer A countdown clock. See DATA_CIA_TIMER_A for more info.
WorkCurrentVoice: .byte 0 // Is set once, but doesn't appear to be referenced after being set
WorkNoteJiffiesLeft: .byte 0,0,0 // Jiffys remaining until this note is done playing. See chart on Page 122 of Compute Book
WorkVoiceForCallPhrase: .byte 0,0,0
WorkVoiceNoteTie: .byte 0,0,0
WorkReleasePoint: .byte 0,0,0
WorkHoldTime: .byte 0,0,0
WorkHoldTimeJifsLeft: .byte 0,0,0
WorkPortamentoTmp: .byte 0,0,0
WorkPortamentoLo: .byte 0,0,0
WorkPortamentoHi: .byte 0,0,0
WorkVibratoDepthA: .byte 0,0,0
WorkVibratoDepthB: .byte 0,0,0
WorkVibratoDepthC: .byte 0,0,0
WorkVibratoDepthD: .byte 0,0,0
WorkVibratoDepthE: .byte 0,0,0
WorkVibratoDepthF: .byte 0,0,0
WorkVibratoRate: .byte 0,0,0
WorkPortAndVibrato: .byte 0,0,0
WorkPVDA: .byte 0,0,0
WorkPVDB: .byte 0,0,0
WorkPVDC: .byte 0,0,0
WorkPVDD: .byte 0,0,0
WorkPCDState: .byte 0,0,0
WorkPulseVibratoRate: .byte 0,0,0
WorkPulseVibratoDepth: .byte 0,0,0
WorkTPSA: .byte 0,0,0
WorkTPSB: .byte 0,0,0
WorkTPSC: .byte 0,0,0
WorkTPSD: .byte 0,0,0
WorkTPSHalfSteps: .byte 0,0,0
WorkDetuneLo: .byte 0,0,0
WorkDetuneHi: .byte 0,0,0
WorkNoteFreqLo: .byte 0,0,0
WorkNoteFreqHi: .byte 0,0,0
WorkPulseSweep: .byte 0,0,0
WorkPulseSweepOther: .byte 0,0,0
WorkPulseWidthLo: .byte 0,0,0
WorkPulseWidthHi: .byte 0,0,0
WorkAutoFilter: .byte 0,0,0
WorkFilterSweep: .byte 0,0,0
WorkFilterCutoffVc: .byte 0,0,0
WorkNxtCmdLo: .byte 0,0,0 // Lo and Hi of Memory Address of next Command byte
WorkNxtCmdHi: .byte 0,0,0 // For voice 1,2 and 3
WorkVoiceForHEDDef: .byte 0,0,0
WorkCmdAfterHEDLo: .byte 0,0,0
WorkCmdAfterHEDHi: .byte 0,0,0
WorkHEDRepeatCnt: .byte 0,0,0 // How many repeats are left
WorkPhraseCallStack: .byte 0,0,0
WorkLFO: .byte 0
WorkOscillatorVoice3: .byte 0
WorkEnvGenOutputVoice3: .byte 0
WorkVoiceWavSrc: .byte 0,0,0 // 0-2. Set by the SRC command 0=Software-Generated waveform 1=OSC3 register 2=ENV3 register
WorkVoiceWavDst: .byte 0,0,0 // 0-3. Set by the DST command 0=Modulation Off,1=Frequency,2=Pulse Width,3=Filter Cutoff
WorkVoiceWavScale: .byte 0,0,0 // -7 to 7. SEt by the SCA command:
WorkLFOC: .byte 0
WorkLFORampUp: .byte 0 // (Value from 0..31) Set with RUP
WorkLFORampDown: .byte 0 // (Value from 0..31) Set with RDN
WorkMaxModulation: .byte 0 // (Value from 0..255) set with MAX
WorkVoiceDSTA: .byte 0,0,0
WorkVoiceDSTB: .byte 0,0,0
WorkVoiceDSTC: .byte 0,0,0
WorkVoiceDSTD: .byte 0,0,0
WorkGlobalDSTA: .byte 0
.byte 0,0 // Unused
WorkGlobalDSTB: .byte 0
.byte 0,0 // Unused
WorkLFOA: .byte 0
WorkLFOB: .byte 0
WorkCallStkNxtCmdAdrLo: .fill 15,0 // Addresses Lo of next command. voice1=0-4 voice2=5=9 voice3=10=14. Used when calling phrases from phrases
WorkCallStkNxtCmdAdrHi: .fill 15,0 // Addresses Hi of next command. voice1=0-4 voice2=5=9 voice3=10=14. Used when calling phrases from phrases
PhraseDefLo: .fill 24,0 // Lo Byte of the stored address where the phrase notes are located
PhraseDefHi: .fill 24,0 // Hi Byte of the stored address where the phrase notes are located
WorkCallStkState: .fill 15,0 // END OF WORK MEMORY
WorkPhraseUnknown: .fill 24,0 // Initialized with 24 $FFs when a song starts
* = * "Preserved Data"
PRESERVE_ZP_FB: .byte 0
PRESERVE_ZP_FC: .byte 0
PRESERVE_ZP_FD: .byte 0
PRESERVE_ZP_FE: .byte 0
PRESERVE_ZP_FF: .byte 0
DATA_CIA_TIMER_A: .byte 0,0 // Cycles per Jiffy for the CIA Timer. (cps = Cycles Per Second) / 60 to get how cycles per jiffy (jiffy = 1/60 second)
// On PAL: 16,420 (PAL CPU = 985,248.5 cps / 60 = 16,420 (.80) rounds to 16,420)
// On NTSC: 17,045 (NTSC CPU = 1,022,727 cps / 60 = 17,045.45 rounded to 17,045)
DATA_OLD_IRQH: .byte 0,0
* = * "User Defined Auxiliary Routine"
AuxRoutineAddrLoHi: .byte <DefaultAuxRoutine
.byte >DefaultAuxRoutine
DefaultAuxRoutine: rts // See Command: AUX for more details A(A=0-255) X=Voice Number (0-2)
* = * "Lookup Tables"
DataVoiceBitMask: .byte $01,$02,$04 // 001 010 100 (This is loaded and EORed to toggle a voice on or off)
SIDVoiceRegsLo: .byte $00,$07 // 0E below this is part of this lookup. oe is used in the note lookup because it is a rest and is ignored
DataNoteLookup: .byte $0E,$02,$02 // The first 0E (rest) is also used above when looking up lo byte of SID Register for voice 3 ($D40E)
.byte $FE,$02,$02 // 1=C 2=d 3=e 4=f 5=g 6=a 7=b 0=Rest
.byte $FE,$FE
DataAccidentalModifier: .byte $00 // This is a working byte
.byte $01,$00 // 01=Sharp 00 = Normal FF (-1) = Flat?
DataHalfStepsFromC: .byte $FF,$00,$02,$04 // In order (starting with FF) 0=Rest 1=C 2=d 3=e 4=f 5=g 6=a 7=b
.byte $05,$07,$09,$0B // Translates to half step modify to the notes from C r=255 c=0 d=2 e=4 f=5 g=7 a=9 b=11
// Frequency Look up Table : See C64 Programmers Ref Appendix E for table of values
// Index 0 = $861E / 34334 C Octave 7
// Index 11 = $FD2E / 64814 B Octave 7
// To calculate an octave, divide the number in half per octave
DataNoteFreqLo: .byte $1E,$18,$8B,$7E // Frequency Lo Table (Starting at C Octave 7 - See C64 Programmers Ref Appendix E)
.byte $FA,$06,$AC,$F3
.byte $E6,$8F,$F8,$2E
DataNoteFreqHi: .byte $86,$8E,$96,$9F // Frequency Hi Table (Starting at C Octave 7 - See C64 Programmers Ref Appendix E)
.byte $A8,$B3,$BD,$C8
.byte $D4,$E1,$EE,$FD
DataOctaveForCutOff: .byte $8C,$78,$64,$50 // Stored in reverse order.. Octave 7 first then to 0
.byte $3C,$28,$14,$00
DataHalfStepForCutOff: .byte $00,$02,$03,$05
.byte $07,$08,$0A,$0C
.byte $0D,$0F,$11,$12
DataFilterLookupValue: .byte $00,$E0
DataMinCallStack: .byte $00
DataMaxCallStack: .byte 5,10,15
DataTPSLookupA: .byte $F9
DataTPSLookupB: .byte $00,$F5
DataTempoTable: .byte $00,$00,$00,$10
.byte $00,$00,$20,$00
.byte $00,$30,$00,$00
.byte $40,$00,$00,$50
.byte $00,$00,$60,$00
.byte $00,$70,$00,$00
.byte $80,$00,$00,$90
.byte $00,$00,$A0,$00
// -----------------------------------------------------------------
// Hook - install the SID Player IRQ Routine
// -----------------------------------------------------------------
* = * "Install SID Player"
INSTALL_SID_PLAYER: {
lda #$00
sta SID_STATUS // All voices off
ldx #$95 // Set NTSC value ro $95$42 ($4295 / 17045=# of cpu cycles for NTSC to refresh)
ntsc: ldy #$42 // NTSC CPU = 1,022,727 cycles/second / 60 = 17,045.45 rounded to 17,045
lda $02A6 // PAL/NTSC Flag 0=NTSC / 1=PAL
beq setCIATimerA
pal: ldx #$25 // Set PAL value to $25$40 ($4025 / 16421=# of cpu cycles for PAL to refresh)
ldy #$40 // PAL CPU = 985,248.5 / 60 = 16,420.808333... rounds up to 16,421
setCIATimerA: stx DATA_CIA_TIMER_A // Don't think confuse this with 50hz video refresh; this routine is designed to run 60/second
sty DATA_CIA_TIMER_A+1
lda $0314
sta DATA_OLD_IRQH
lda $0315
sta DATA_OLD_IRQH + 1
sei
lda #<IRQ_Handler
sta $0314
lda #>IRQ_Handler
sta $0315
cli
rts
}
// -----------------------------------------------------------------
// Play - Initialize the player to play a new song.
// Clears working memory, assigns default values and sets up
// pointers to the voice data.
// Inputs: x=lo y=hi of memory address of song to play
// Outputs: x=lo y=hi of string of song info (null terminated text)
// ZP Usage: $61,$62 lo,hi of address of music in Zero Page (Not preserved)
// $63,$64 lo,hi of D400 - start of SID registers - used to set default values quickly.
// $61-$65 is normally the Floating Point Accumulator #1 used by BASIC.
// -----------------------------------------------------------------
* = * "Initialize Player to Play Song"
INIT_SONG: {
lda #$00
sta SID_STATUS // Turn off all voices
stx $61 // Store lo,hi of address of music in zero page
sty $62
ldy #$bc
clearWorkMemLoop: sta SID_STATUS,y // Set 0s to memory addresses between START end END OF WORK MEMORY
dey
bne clearWorkMemLoop
ldy #$72
clearWorkMemLoop2: sta WorkPhraseCallStack + 2,y
dey
bne clearWorkMemLoop2
sta SID.filterCutoffLo // Clear Filter Cutoff Frequency lo
sta SID.filterCutoffHi // Clear Filter Cutoff Frequency hi
lda #%00001000
sta WorkFilterControl
sta SID.filterResonanceControl // Filters off on all voices. Enable processing an external audio signal which is brought in through pin 5 of the Audio/Video P6rt.
sta WorkVolAndFilter
sta SID.filterVolumeAndFilter // Set volume to 8 out of 15 and turn off all filters
lda #$90
sta WorkTempo
lda #$60
sta WorkTempoTableValue
lda #$0c
sta WorkUTLValue
lda DATA_CIA_TIMER_A // Set the CIA Timer to the hardware detected default values (PAL vs NTSC)
sta WorkCiaTimerA // See DATA_CIA_TIMER_A for more info
lda DATA_CIA_TIMER_A + 1
sta WorkCiaTimerA + 1
lda #$ff
sta WorkMaxModulation
lda #$d4 // Use ZP $D3,$D4 to write to SID Register $D4xx
sta $64
ldx #$02
setInitVoiceValuesLoop: lda #$ff
sta WorkMeasureHi,x
lda #$01
sta WorkNoteJiffiesLeft,x
sta WorkUTVValues,x
txa
sta WorkVoiceForCallPhrase,x
sta WorkVoiceForHEDDef,x
lda #$04 // Set default release point in the ADSR envelope to 4
sta WorkReleasePoint,x
lda DataMinCallStack,x
sta WorkPhraseCallStack,x
lda #$5b
sta WorkTPSC,x
lda SIDVoiceRegsLo,x
sta $63
lda #$00
tay
sta ($63),y
iny
sta ($63),y
iny
sta ($63),y
lda #$08
sta WorkPulseWidth2Hi,x
sta WorkPulseWidthHi,x
iny
sta ($63),y
iny
sta ($63),y
lda #$40
sta WorkVc1Control,x
sta ($63),y
lda #$20
sta WorkAttackDecays,x
iny
sta ($63),y
lda #$f5
sta WorkSusRelease,x
iny
sta ($63),y
dex
bpl setInitVoiceValuesLoop
txa // a = $FF
ldx #23 // Set WorkPhraseUnknown C13E - C155 to $FF (24 memory positions)
initWorkPhraseLoops: sta WorkPhraseUnknown,x
dex
bpl initWorkPhraseLoops
lda $61 // 61,62 contain start address of the song lo,hi
clc //
adc #$06 // add 6 to the low address
sta $63 // 63,64 will be the start of voice 1 data
lda #$00
tax
tay
adc $62
addressOfVoiceLoop: sta $64 // a= hi of voice x data
sta WorkNxtCmdHi,x // store hi of voice x data
sta WorkCmdAfterHEDHi,x // store hi of voice x data
lda $63 // a= lo of voice x data
sta WorkNxtCmdLo,x // store lo of voice 1+x data
sta WorkCmdAfterHEDLo,x // store lo of voice 1+x data
clc
adc ($61),y
sta $63
lda $64
iny
adc ($61),y
iny
inx
cpx #$03
bne addressOfVoiceLoop
ldx $63 // x=lo a,y=hi address of text area for song info
tay
rts
}
// -----------------------------------------------------------------
// Hush
// Silence the three voices and to reset the hardware timer interrupt rate.
// -----------------------------------------------------------------
* = * "Hush (Silence Voices, reset timers)"
HUSH_PLAYER: lda #$00
sta SID.voice1Control
sta SID.voice2Control
sta SID.voice3Control
sta SID.voice1FrequencyHi // Voice 1 Frequency Control (high byte)
sta SID.voice2FrequencyHi // Voice 2 Frequency Control (high byte)
sta SID.voice3FrequencyHi // Voice 3 Frequency Control (high byte)
lda #%00001000
sta SID.filterResonanceControl // Filters off on all voices. Enable processing an external audio signal which is brought in through pin 5 of the Audio/Video P6rt.
lda DATA_CIA_TIMER_A
sta $dc04 // CIA TIMER A LO
lda DATA_CIA_TIMER_A + 1
sta $dc05 // CIA TIMER A HI
rts
// -----------------------------------------------------------------
// Drop
// Undoes everything done by HOOK and restores the interrupt processing to normal.
// -----------------------------------------------------------------
* = * "Drop (Uninstall IRQ Handler)"
REMOVE_SID_PLAYER: sei
lda DATA_OLD_IRQH
sta $0314
lda DATA_OLD_IRQH + 1
sta $0315
cli
rts
// -----------------------------------------------------------------
// Exit IRQ (And optionally stop all voices)
// Called by IRQ_Handler
// -----------------------------------------------------------------
StopMusicAndExitIRQ: lda #%00001000 // Set all 3 voices to off. Bit 3 signals the IRQ Handler was called while it was running
sta SID_STATUS
ExitIRQ: jmp (DATA_OLD_IRQH)
// -----------------------------------------------------------------
// Interrupt Handler for the Player
// Called by default 60 times a second (Can be changed with Command: JIF)
// -----------------------------------------------------------------
* = * "IRQ Handler"
IRQ_Handler: lda $dc0d // Clear CIA 1 Interrupt Control Register
lda SID_STATUS
bmi StopMusicAndExitIRQ // Exit the IRQ Handler if bit 7 is set.
ora #%10000000 // Set bit 7 and existing voices (bit 0-2)
tay // Store above in y
and #%00000111 // Test if any voice is on (bit 0-2)
beq ExitIRQ // No voices are on - Exit IRQ
cld
sty SID_STATUS // Set SID_STATUS to be bit 7 on and whatever voices are already on
cli
lda $fb // Preserve Zero Page FB - FF
sta PRESERVE_ZP_FB
lda $fc
sta PRESERVE_ZP_FC
lda $fd
sta PRESERVE_ZP_FD
lda $fe
sta PRESERVE_ZP_FE
lda $ff
sta PRESERVE_ZP_FF
lda WorkFilterSweepState
clc
adc WorkGlobalDSTA
pha
and #$07
tay
lda WorkGlobalDSTB
adc #$00
sta $ff
pla
lsr $ff
ror
lsr $ff
ror
lsr $ff
ror
clc
adc WorkFilterCutOff
sty SID.filterCutoffLo // Lo Byte of Filter Cutoff Frequency
sta SID.filterCutoffHi // Hi Byte of Filter Cutoff Frequency
lda WorkFilterControl
sta SID.filterResonanceControl
lda WorkVolAndFilter
sta SID.filterVolumeAndFilter // Volume and Filter Select Register
lda #$d4
sta $fc
ldx #$00 // fb,fc will be used to quickly set $d400 (Start of SID voice Registers) in the next section
// X is now also 0 which is used to index data by voice
{ // Apply current values to the SID Register for each voice
applyToSIDRegsLoop: lda SID_STATUS // Loop for each voice (x=0..2)
and DataVoiceBitMask,x // Check to see if voice x is on
beq goToNextVoice
lda SIDVoiceRegsLo,x
sta $fb // fb,fc is now setup for voice x registers ($D400=Voice 0, $D407=Voice 1, $D40E=Voice 2)
lda WorkTargetNoteFreqLo,x
clc
adc WorkVibratoDepthC,x
tay
lda WorkTargetNoteFreqHi,x
adc WorkVibratoDepthD,x
pha
tya
clc
adc WorkVoiceDSTA,x
ldy #$00
sta ($fb),y // $D400,$D407, $D40E Voice x Frequency Control (Lo byte)
pla
adc WorkVoiceDSTB,x
iny
sta ($fb),y // $D401,$D408, $D40F Voice x Frequency Control (Hi byte)
lda WorkPulseWidth2Lo,x
clc
adc WorkPVDC,x
sta $ff
lda WorkPulseWidth2Hi,x
adc WorkPVDD,x
pha
lda $ff
clc
adc WorkVoiceDSTC,x
iny
sta ($fb),y // $D402,$D409, $D410 Voice x Pulse Waveform Width (Lo byte)
pla
adc WorkVoiceDSTD,x
iny
sta ($fb),y // $D403,$D40A, $D411 Voice x Pulse Waveform Width (Hi byte)
lda WorkAttackDecays,x
iny
iny
sta ($fb),y // $D405,$D40C, $D413 Voice x Attack/Decay Register
lda WorkSusRelease,x
iny
sta ($fb),y // $D406,$D40D, $D414 Voice x Sustain/Release Control Register
goToNextVoice: inx
cpx #$03
bne applyToSIDRegsLoop
ldy WorkVc1Control //Set all three voices control register at once. These registers...
ldx WorkVc2Control // start/stop sound
lda WorkVc3Control // Select Wave Forms (Triangle, Noise, Sawtooth, Pulse, noise and combinations of each)
sty SID.voice1Control
stx SID.voice2Control // Ring modulate with oscillators 1 and 3
sta SID.voice3Control // Disable oscillator 1
}
// Prepare the CIA Timer to trigger the next IRQ
// The only way the timer countdown will change is from a JIF command
ldx WorkCiaTimerA // Set a new timer value to the CIA Timer A
ldy WorkCiaTimerA + 1 // This value takes effect when the current timer hits 0
stx $dc04 // At that point, this new value will be used for the count down
sty $dc05 // By default / this is 1/60th of a second. See DATA_CIA_TIMER_A for more info
lda $d41b // Read Oscillator Voice 3 / Random Number Generator
sta WorkOscillatorVoice3
lda $d41c // Envelope Generator Voice 3 Output
sta WorkEnvGenOutputVoice3
// This calls a subroutine to process each voice.
// It will pull in the next command, run it and do so until it a note plays or it hits end of song for the voice
ldx #$00
{
processVoiceLoop: lda SID_STATUS
and DataVoiceBitMask,x
beq goToNextVoice
stx WorkCurrentVoice // Even though it is set here, nothing else in the code appears to ever read this.
jsr ProcessVoice
lda SID_STATUS
and #%01111000 // Are any of the error status bits set?
beq goToNextVoice // No errors, let's continue to the next voice
jmp CleanUpAndExitIRQ // If there any errors, (all voices will be shutoff) we can continue on. The player will stop playing the song.
goToNextVoice: inx
cpx #$03
bne processVoiceLoop
}
// This section of code is to process effects that are global to all voices
lda WorkLFOC // LFO = Low Frequency Oscillation (See Command: LFO)
bne lFOBLogic
lda WorkLFORampUp
ora WorkLFORampDown
beq voiceWavDstLogic
lda WorkLFOA
bne lfoSkipAhead
lFORampupLogic: lda WorkLFORampUp
beq lfoRampdownLogic
clc
adc WorkLFO
bcs moreLFOLogic
cmp WorkMaxModulation
bcc endOfLGOLogic
beq endOfLGOLogic
moreLFOLogic: lda #$00
sta WorkLFOA
lda WorkLFORampDown
beq endOfLGOLogic
inc WorkLFOA
lda WorkLFO
sbc WorkLFORampDown
jmp endOfLGOLogic
lfoSkipAhead: lda WorkLFORampDown
beq lFORampupLogic
lfoRampdownLogic: lda WorkLFO
sec
sbc WorkLFORampDown
bcs endOfLGOLogic
lda #$00
sta WorkLFOA
lda WorkLFORampUp
bne endOfLGOLogic
inc WorkLFOA
bne skipToEnd
lFOBLogic: dec WorkLFOB
bne voiceWavDstLogic
lda WorkLFOA
bne lowerLFOA
inc WorkLFOA
lda WorkLFORampDown
bne storeLFBOTemp
lda #$20
storeLFBOTemp: sta WorkLFOB
lda #$00
beq endOfLGOLogic
lowerLFOA: dec WorkLFOA
lda WorkLFORampUp
bne endOfLGOBCalc
lda #$20
endOfLGOBCalc: sta WorkLFOB
skipToEnd: lda WorkMaxModulation
endOfLGOLogic: sta WorkLFO
// Apply Command: DST logic (Loops through the voices)
voiceWavDstLogic: { ldx #$00
wavDSTVoiceLoop: lda WorkVoiceWavDst,x
beq nextDSTVoiceLoop
lda #$00
sta $ff
ldy WorkVoiceWavSrc,x
lda WorkLFO,y
ldy WorkVoiceWavScale,x
beq skipAheadToDST
bmi dstloop
wavLoop: asl
rol $ff
dey
bne wavLoop
beq skipAheadToDST
dstloop: lsr
iny
bne dstloop
skipAheadToDST: ldy WorkVoiceWavDst,x
dey
bne skipAheadToDSTC
sta WorkVoiceDSTA,x
lda $ff
sta WorkVoiceDSTB,x
jmp nextDSTVoiceLoop
skipAheadToDSTC: dey
bne skipAheadToDSTA
sta WorkVoiceDSTC,x
lda $ff
sta WorkVoiceDSTD,x
jmp nextDSTVoiceLoop
skipAheadToDSTA: sta WorkGlobalDSTA
lda $ff
sta WorkGlobalDSTB
nextDSTVoiceLoop: inx
cpx #$03
bne wavDSTVoiceLoop
}
// End of the IRQ Handler
// Restore the Zero Page FB..FF
// Store a new SID_STATUS in case an error occurred
lda SID_STATUS
and #%01111111 // Turn bit 7 off to signal the IRQ Handler is done working
CleanUpAndExitIRQ: sta SID_STATUS
lda PRESERVE_ZP_FB // Restore the preserved Zero Page locations
sta $fb
lda PRESERVE_ZP_FC
sta $fc
lda PRESERVE_ZP_FD
sta $fd
lda PRESERVE_ZP_FE
sta $fe
lda PRESERVE_ZP_FF
sta $ff
jmp (DATA_OLD_IRQH)
workNoteTieIsNegative: lda WorkPortAndVibrato,x
bne jumpAhead
jmp endOfProcessNote
jumpAhead: jmp vibratoDepthLogic
// This is called from processVoiceLoop (Above) / x= voice #
ProcessVoice: dec WorkNoteJiffiesLeft,x // These start at 1 on song init. Number of Jiffys left to left the note play out.
bne NoteIsPlaying
jmp SetCurrentCmdAddress
NoteIsPlaying: lda WorkVoiceNoteTie,x
bmi workNoteTieIsNegative
bne portamentoLogic
lda WorkHoldTimeJifsLeft,x // From Command:HLD
beq releasePointLogic
dec WorkHoldTimeJifsLeft,x
bne portamentoLogic
releasePointLogic: lda WorkReleasePoint,x // Default value is 4 - change with Command: PNT
cmp WorkNoteJiffiesLeft,x
bcc portamentoLogic
lda WorkVc1Control,x
and #%11111110 // Start the release phase of the ADSR for this voice
sta WorkVc1Control,x
portamentoLogic: lda WorkPortamentoTmp,x
beq vibratoDepthLogic
asl
lda WorkTargetNoteFreqLo,x
bcs portaMentoLogic
adc WorkPortamentoLo,x
sta WorkTargetNoteFreqLo,x
tay
lda WorkTargetNoteFreqHi,x
adc WorkPortamentoHi,x
sta WorkTargetNoteFreqHi,x
pha
tya
cmp WorkNoteFreqLo,x
pla
sbc WorkNoteFreqHi,x
bcs portaMentoLogic2
bcc portaMentoLogic3
portaMentoLogic: sbc WorkPortamentoLo,x
sta WorkTargetNoteFreqLo,x
lda WorkTargetNoteFreqHi,x
sbc WorkPortamentoHi,x
sta WorkTargetNoteFreqHi,x
lda WorkNoteFreqLo,x
cmp WorkTargetNoteFreqLo,x
lda WorkNoteFreqHi,x
sbc WorkTargetNoteFreqHi,x
bcc portaMentoLogic3
portaMentoLogic2: lda WorkNoteFreqLo,x
sta WorkTargetNoteFreqLo,x
lda WorkNoteFreqHi,x
sta WorkTargetNoteFreqHi,x
lda #$00
sta WorkPortamentoTmp,x
portaMentoLogic3: lda WorkPortAndVibrato,x
beq pulseSweepLogic
vibratoDepthLogic: lda WorkVibratoDepthA,x
beq endVibratoLogic
ldy #$00
dec WorkVibratoDepthB,x
bne vibDepthLogic3
lda WorkVibratoDepthC,x
ora WorkVibratoDepthD,x
bne vibDepthLogic2a
lda WorkVibratoRate,x
sta WorkVibratoDepthE,x
sta WorkVibratoDepthB,x
lda WorkVibratoDepthA,x
asl
lda WorkVibratoDepthF,x
bcc vibDepthLogic2
eor #$ff
adc #$00
vibDepthLogic2: sta WorkVibratoDepthA,x
bne vibDepthLogic3a
vibDepthLogic2a: lda WorkVibratoDepthE,x
sta WorkVibratoDepthB,x
tya
sec
sbc WorkVibratoDepthA,x
sta WorkVibratoDepthA,x
vibDepthLogic3: cmp #$00
vibDepthLogic3a: bpl vibDepthLogic4
dey
vibDepthLogic4: clc
adc WorkVibratoDepthC,x
sta WorkVibratoDepthC,x
tya
adc WorkVibratoDepthD,x
sta WorkVibratoDepthD,x
endVibratoLogic: lda WorkVoiceNoteTie,x
bmi pvdLogic // Is bit 7 set? Double dotted note
pulseSweepLogic: lda WorkPulseSweep,x // See Command: P-S
beq pvdLogic
clc
adc WorkPulseWidth2Lo,x
sta WorkPulseWidth2Lo,x
lda WorkPulseSweepOther,x
adc WorkPulseWidth2Hi,x
sta WorkPulseWidth2Hi,x
pvdLogic: lda WorkPVDA,x // See Command: PVD Pulse vibrato depth
beq checkForNoteTie
ldy #$00
dec WorkPVDB,x
bne skipAheadPVDB
lda WorkPVDC,x
ora WorkPVDD,x
bne pCDStateLogic
lda WorkPulseVibratoRate,x
sta WorkPCDState,x
sta WorkPVDB,x
lda WorkPVDA,x
asl
lda WorkPulseVibratoDepth,x
bcc storePVDAResult
eor #$ff
adc #$00
storePVDAResult: sta WorkPVDA,x
bne pvdcLogic
pCDStateLogic: lda WorkPCDState,x
sta WorkPVDB,x
tya
sec
sbc WorkPVDA,x
sta WorkPVDA,x
skipAheadPVDB: cmp #$00
pvdcLogic: bpl skipAheadPVDC
dey
skipAheadPVDC: clc
adc WorkPVDC,x
sta WorkPVDC,x
tya
adc WorkPVDD,x
sta WorkPVDD,x
checkForNoteTie: lda WorkVoiceNoteTie,x // Tie is bit 6 -(bit 7 is double dot, which would make this negative)
bpl filterSweepLogic // If we are a tie not, keep the note playing.
jmp endOfProcessNote
filterSweepLogic: ldy #$00
lda WorkFilterSweep,x
beq endOfProcessNote
bpl sweepLoop
iny
sweepLoop: clc
adc WorkFilterSweepState
pha
and #$07
sta WorkFilterSweepState
pla
ror
lsr
lsr
clc
adc DataFilterLookupValue,y
clc
adc WorkFilterCutOff
sta WorkFilterCutOff
endOfProcessNote: rts
// This will move the next command into the current command
// The init song routine defaults these values to the same address (Start of Voice x)
// Called from ProcessVoice
SetCurrentCmdAddress: lda WorkNxtCmdLo,x // Load the next command lo/hi memory address into the fd,fe (Current command memory address)
sta $fd
lda WorkNxtCmdHi,x
sta $fe
bne ProcessCurrentCmd
RtsToProcessVoice: rts
ProcessCmdLoop: jsr RouteCommand
ProcessCurrentCmd: lda SID_STATUS
and DataVoiceBitMask,x
beq RtsToProcessVoice
ldy #$00
lda ($fd),y // Fetch current command byte from (fd,fe)
sta $ff // Store the current command byte in $FF
iny
lda ($fd),y // Fetch next option by for the command from (fd,fe)
tay
lda $fd // Increase address in fd,fe by 2 - move it to the
clc // start of the next command byte
adc #$02
sta $fd // Store lo addrss of next comand
sta WorkNxtCmdLo,x
lda $fe
adc #$00
sta $fe // Store hi address of next command
sta WorkNxtCmdHi,x
lda $ff // $ff=Current command to execute A=Command Y=Option X=voice # (0..2)
DecodeCommand: and #%00000011 // ****
bne ProcessCmdLoop // Notes always end in 00. 01,10 and 11 are commands.
// Prepare to Play a note
lda WorkNoteFreqLo,x
sta WorkTargetNoteFreqLo,x
lda WorkNoteFreqHi,x
sta WorkTargetNoteFreqHi,x
lda $ff // Reload the command byte into a
// Command: Play a note
sta WorkNoteDuration,x
tya
sta WorkTypeOctNote,x // Accidental aa: 10 Normal 01=Sharp 11=Flat Octive ooo: [EOR of 0-7] Note nnn: C=001 d=010 e=011 f=100 g=101 a=110 b=111
and #%00000111 // Isolate the Note 0000111
tay
lda DataNoteLookup,y // Load something about the note indexed by y where 1=c, 2=d... (We get a $02 or an $FE (a 2 or a -2)
sta DataAccidentalModifier
lda WorkTypeOctNote,x
and #%00111000 // Isolate the Octive 00111000
lsr
lsr
lsr // Move the bits to positions 0-2
adc WorkTPSD,x
sta $fd // FD = Octive (Numbers in bits 0-2) Octive is 0 based
lda WorkTypeOctNote,x
and #%11000000 // Isolate the Type 11000000 (10 Normal 01=Sharp 11=Flat)
asl // Move the 1 into the carry
rol
rol // Rotate twice to move the bits into positions 0-2
tay
lda DataAccidentalModifier,y // Lookup modifyer to make it 1=Sharp, 2=Normal, 3=Flat *****
sta $fe // FE = Accidental Modifier We get back 01=Sharp 00 = Normal FF (-1) = Flat
lda WorkTypeOctNote,x
and #%00000111 // Isolate the Note 0000111
beq ProcessNoteSkipAhead // Branch if it is a rest (Note 000 is a rest)
tay // Put the Note index in y (0=Rest, 1=C, 2=D etc...)
lda DataHalfStepsFromC,y // Translates to a halfstep modifier from C where r=255 c=0 d=2 e=4 f=5 g=7 a=9 b=11
adc $fe // Add in the accidental modifer
clc
adc WorkTPSHalfSteps,x // Add in the TPS Value (Transpose note by number of half steps)
bpl areWeTooManyHalfSteps // Are we 0 or more halfsteps above c?
negativeHalfStepsFromC: clc // We have negative halfsteps so...
adc #12 // Add 12 half steps to bring us up to a positive number
inc $fd // We need to move down an octive and add 12 halfsteps from the previous octive (Octive is stored in EORed format so up is down)
areWeTooManyHalfSteps: cmp #12
bcc storeFinalHalfStepCalc // If somehow we are over 12 in the new octive..
sbc #12 // Roll it back to where we were
dec $fd
storeFinalHalfStepCalc: sta $fe // FE = The final halfstep modifer
tay // Y= Half Steps above C
lda DataNoteFreqHi,y // C = $86 / 134 (Frequency hi)
sta $ff
lda DataNoteFreqLo,y // C = $1E / 30 (Frequency Lo)
ldy $fd // Y = Octive
dey // Divide the Frequency in half by # of octives
bmi applyDetuning
halfPerOctiveLoop: lsr $ff // Divide hi bit half
ror // Rotate the carry into the frequency lo
dey
bpl halfPerOctiveLoop
applyDetuning: clc // At this point, $FF = Freq Hi A= Freq Lo (From Command: DTN)
adc WorkDetuneLo,x // Adjust by DetuneLo
sta WorkNoteFreqLo,x
lda $ff // Adjust by DetuneHi
adc WorkDetuneHi,x
sta WorkNoteFreqHi,x
lda WorkNoteDuration,x
bne applyPortamento
jmp SetCurrentCmdAddress // Skip ahead if somehow this is a zero (shouldn't be possible)
applyPortamento: lda WorkPortamentoLo,x // Portamento is when a note gradually slides into the next one
ora WorkPortamentoHi,x
beq finalizeNoteFreq // No Portamento, skip ahead to finalizing the frequencey we are going to play
lda WorkTargetNoteFreqLo,x
cmp WorkNoteFreqLo,x
lda WorkTargetNoteFreqHi,x
sbc WorkNoteFreqHi,x
lda #$fe // FE = The final halfstep modifer
ror
sta WorkPortamentoTmp,x
bcc processTieNote
ProcessNoteSkipAhead: beq calculateNoteDuration
finalizeNoteFreq: sta WorkPortamentoTmp,x
lda WorkNoteFreqLo,x
sta WorkTargetNoteFreqLo,x
lda WorkNoteFreqHi,x
sta WorkTargetNoteFreqHi,x
processTieNote: lda WorkVoiceNoteTie,x
asl
bne calculateNoteDuration
lda WorkPulseSweep,x
beq processAutoFilter
lda WorkPulseWidthLo,x
sta WorkPulseWidth2Lo,x
lda WorkPulseWidthHi,x
sta WorkPulseWidth2Hi,x
processAutoFilter: lda WorkAutoFilter,x
beq processFilterSweep
ldy $fd // fd = Octive #
clc
adc DataOctaveForCutOff,y
ldy $fe // fe = Half step modifier
clc
adc DataHalfStepForCutOff,y
clc
bcc processFilterCutoff
processFilterSweep: lda WorkFilterSweep,x
beq calculateNoteDuration
lda WorkFilterCutoffVc,x
processFilterCutoff: sta WorkFilterCutOff