Merlin32, midi-style player code

added merlin32 source file, documentation and example song for
midi-style player, fixed bugs
This commit is contained in:
Charles Mangin 2016-12-27 22:12:49 -05:00
parent 23be395102
commit d64403c376
6 changed files with 199 additions and 30 deletions

Binary file not shown.

View File

@ -1,7 +1,7 @@
# KSynth
Beginnings of a dual tone synthesizer for the Apple II
All the code is in this README, since it's small enough to copy and paste into an emulator for now.
All the code is in this README, since it's small enough to copy and paste into an emulator for now. There is also a Merlin32 compatible source file, ksynth.s, with comments and equivalents to make it more relocatable.
The included DSK file contains the following files:
@ -10,13 +10,14 @@ The included DSK file contains the following files:
*BFLATSCALE.SONG*: B-Flat scale
*VICTORY.SONG*: "Victory" tune from Karateka
*DAISYBELL.SONG*: "Bicylce Built for Two" AKA "Daisy Bell"
*DAISYBELL.MIDI*: "Bicylce Built for Two" AKA "Daisy Bell" in MIDI-like format for KSYNTH.MIDI
*KSYNTH.MIDI*: MIDI-style KSYNTH player routine. Looks up notes from MIDI.LOOKUP instead of raw cycle counts.
*MIDI.LOOKUP*: Lookup table of notes to cycle counts for MIDI style player.
*RANDOMNOTES*: A routine to play random tones with the KSYNTH routine.
*MIDI.LOOKUP2*: Alternate MIDI lookup table that covers larger range of tones
*TWOTONE.PLAYER*: Slightly better MIDI-style player routine. Start here.
*TWOTONE.PLAYER*: Slightly different MIDI-style player. Plays arbitrary two-note combinations.
To do: Add MIDI example songs.
To do: Add more MIDI example songs.
@ -146,6 +147,8 @@ Using the above table as a starting point, a MIDI lookup table can be created. M
By shifting 12 bytes up or down (one octave), a song's range can be easily be changed to fit in KSYNTH's limited range.
```
notes and octaves
C C# D D# E F F# G G# A A# B
0 00 00 00 00 00 00 00 00 00 00 00 00
1 00 00 00 00 00 00 00 00 00 00 00 00
@ -158,11 +161,27 @@ By shifting 12 bytes up or down (one octave), a song's range can be easily be ch
8 00 00 00 00 00 00 00 00 00 00 00 00
9 00 00 00 00 00 00 00 00
bytes
$30 00 00 00 FC EF E1 D5 C9 BD B3 A9 9F 96 8E 86 7E
$40 77 71 6A 64 5E 59 54 4F 4B 47 43 3F 3C 38 35 32
$50 2F 2D 2A 27 25 23 21 1F 1E 1C 1A 19 17 16 15 13
1133: FC EF E1 D5 C9 BD B3 A9 9F 96 8E 86 7E 77 71 6A 64 5E 59 54 4F 4B 47 43 3F 3C 38 35 32 2F 2D 2A 27 25 23 21 1F 1E 1C 1A 19 17 16 15 13
```
##Daisy Bell (Bicycle Built For Two) MIDI style##
##MIDI-like Player##
1000: 30 41 30 3E 30 3A 30 35 10 37 10 39 10 3A 20 37 10 3A 60 35 30 3C 30 41 30 3E 30 3A 10 37 10 39 10 3A 20 3C 10 3E 40 3C 10 FF 10 3E 10 40 10 3E
1030: 10 3C 20 41 10 3E 10 3C 40 3A 10 3C 20 3E 10 3A 20 37 10 3A 10 37 40 35 10 35 20 3A 10 3E 10 3C 20 FF 20 3A 10 3E 10 3C 10 FF 5 3E 5 40 10 41 10 3E 10 3A 20 3C 10 35 40 3A 00 00
##KSYNTH.MIDI: MIDI-like Player##
_assumes song src at $1000_
@ -171,37 +190,36 @@ _assumes lookup table src at $1100_
```
0330- A9 00 LDA #$00 ; start at zero
0332- AA TAX ; X = 0; LOOP
0332- AA TAX ; X = 0 ; LOOP
0333- BD 00 10 LDA $1000,X ; load note duration
0336- F0 31 BEQ $0369 ; if note is 0 duration, end the song
0336- F0 35 BEQ $036D ; if note is 0 duration, end the song
0338- 85 FA STA $FA ; store duration at $FA
033A- E8 INX ; increment pointer to note value
033B- BD 00 10 LDA $1000,X ; load note MIDI-style value
TAY ; lookup note loop value from lookup table
LDA $1100,Y ;
033E- 85 FB STA $FB ; store note value at $FB
0340- 85 FD STA $FD ; store note value at $FD
0342- C9 FF CMP #$FF ; if note value is FF, rest
0344- D0 06 BNE $034C ; skip over if !=FF
0346- 8D 1E 03 STA $031E ; change the $C030 click to BIT $FF30
0349- 8D 13 03 STA $0313 ; change the $C030 click to BIT $FF30
034C- C6 FD DEC $FD ; decrement $FD for That Karateka Sound™
034E- 8A TXA ; put current note pointer in Accumulator
034F- 85 FF STA $FF ; store that in $FF
0351- 20 00 03 JSR $0300 ; play the actual note
0354- AD 13 03 LDA $0313 ; did we mess with the C030 click?
0357- C9 FF CMP #$FF ; if it's FF, we did. change it back.
0359- D0 08 BNE $0363 ; skip if !=FF
035B- A9 C0 LDA #$C0 ; set click points back to $c030
035D- 8D 1E 03 STA $031E ;
0360- 8D 13 03 STA $0313 ;
0363- E6 FF INC $FF ; increment to next note address
0365- A5 FF LDA $FF ; load Accumulator with next note address
0367- D0 C9 BNE $0332 ; branch to LOOP
0369- 60 RTS ;
033E- A8 TAY ; lookup note loop value from lookup table
033F- B9 00 11 LDA $1100,Y ;
0342- 85 FB STA $FB ; store note value at $FB
0344- 85 FD STA $FD ; store note value at $FD
0346- C9 FF CMP #$FF ; if note value is FF, rest
0348- D0 06 BNE $0350 ; skip over if !=FF
034A- 8D 1E 03 STA $031E ; change the $C030 click to BIT $FF30
034D- 8D 13 03 STA $0313 ; change the $C030 click to BIT $FF30
0350- C6 FD DEC $FD ; decrement $FD for That Karateka Sound™
0352- 8A TXA ; put current note pointer in Accumulator
0353- 85 FF STA $FF ; store that in $FF
0355- 20 00 03 JSR $0300 ; play the actual note
0358- AD 13 03 LDA $0313 ; did we mess with the C030 click?
035B- C9 FF CMP #$FF ; if it's FF, we did. change it back.
035D- D0 08 BNE $0367 ; skip if !=FF
035F- A9 C0 LDA #$C0 ; set click points back to $c030
0361- 8D 1E 03 STA $031E ;
0364- 8D 13 03 STA $0313 ;
0367- E6 FF INC $FF ; increment to next note address
0369- A5 FF LDA $FF ; load Accumulator with next note address
036B- D0 C5 BNE $0332 ; branch to LOOP
036D- 60 RTS ;
330: A9 00 AA BD 00 10 F0 31 85 FA E8 BD 00 10 A8 B9 00 11 85 FB 85 FD C9 FF D0 06 8D 1E 03 8D 13 03 C6 FD 8A 85 FF 20 00 03 AD 13 03 C9 FF D0 08 A9 C0 8D 1E 03 8D 13 03 E6 FF A5 FF D0 C9 60
330: A9 00 AA BD 00 10 F0 35 85 FA E8 BD 00 10 A8 B9 00 11 85 FB 85 FD C9 FF D0 06 8D 1E 03 8D 13 03 C6 FD 8A 85 FF 20 00 03 AD 13 03 C9 FF D0 08 A9 C0 8D 1E 03 8D 13 03 E6 FF A5 FF D0 C5 60

1
_FileInformation.txt Normal file
View File

@ -0,0 +1 @@
ksynth=Type(00),AuxType(0000),VersionCreate(70),MinVersion(BE),Access(E3),FolderInfo1(000000000000000000000000000000000000),FolderInfo2(000000000000000000000000000000000000)

BIN
ksynth Normal file

Binary file not shown.

73
ksynth.s Normal file
View File

@ -0,0 +1,73 @@
ORG $0300
DURATION EQU $FA ; Note DURATION
XCOUNTER EQU $FB ; X oscillator counter
MULTIPLIER EQU $FC ; duration multiplier (defaults to $ff)
OSCILLATOR EQU $FD ; second oscillator value - decrements once for vibrato
OSCCOUNTER EQU $FE ; tracks second oscillator value
SEQUENCE EQU $FF ; note number/address offset from song origin
SONGORIGIN EQU $1000 ; song header start address
MIDILOOKUP EQU $1100 ; MIDI notes lookup table address
CLICKBIT1 EQU $031E ; modified byte - set to FF for rest note
CLICKBIT2 EQU $0313
GENERATOR LDA OSCILLATOR ; load second oscillator value into $FE
STA OSCCOUNTER ;
LDA #$FF ; load duration multiplier into $FC
STA MULTIPLIER ;
LDY DURATION ; load duration into Y
LDX XCOUNTER ; load oscillator into X
STARTCOUNT DEX ; start countdown first oscillator, X
NOP ; wait...
NOP ;
BNE COUNTDOWN2 ; if X = 0, click. otherwise, skip
CLICK1 BIT $C030 ; click
RESETX LDX XCOUNTER ; reset X to beginning of countdown
COUNTDOWN2 DEC OSCCOUNTER ; countdown second oscillator, $FE
NOP ; wait...
NOP ;
BNE COUNTTIME ; if $FE = 0, click. otherwise, skip
CLICK2 BIT $C030 ; click
RESETOSC2 LDA OSCILLATOR ; reset $FE to beginning of countdown
STA OSCCOUNTER ;
COUNTTIME DEY ; countdown duration
BNE STARTCOUNT ; if duration hasn't expired, return and count down oscillators
LDY DURATION ; if duration has expired, reset duration
COUNTTIME2 DEC MULTIPLIER ; decrement duration multiplier
BNE STARTCOUNT ; if multiplier hasn't expired, return and count down oscillators
RTS ; all done
ORG $0330
PLAYER LDA #$00 ; start at zero
]LOOP TAX ; X = 0 ; LOOP
LDA SONGORIGIN,X ; load note duration
BEQ ENDSONG ; if note is 0 duration, end the song
STA DURATION ; store duration at $FA
INX ; increment pointer to note value
LDA SONGORIGIN,X ; load note MIDI-style value
TAY ; lookup note loop value from lookup table
LDA MIDILOOKUP,Y ;
STA XCOUNTER ; store note value at $FB
STA OSCILLATOR ; store note value at $FD
CMP #$FF ; if note value is FF, rest
BNE SETOSC2 ; skip over if !=FF
STA CLICKBIT1 ; change the $C030 click to BIT $FF30
STA CLICKBIT2 ; change the $C030 click to BIT $FF30
SETOSC2 DEC OSCILLATOR ; decrement $FD for That Karateka Sound™
TXA ; put current note pointer in Accumulator
STA SEQUENCE ; store that in $FF
JSR GENERATOR ; play the actual note
LDA CLICKBIT1 ; did we mess with the C030 click?
CMP #$FF ; if it's FF, we did. change it back.
BNE NEXTNOTE ; skip if !=FF
LDA #$C0 ; set click points back to $c030
STA CLICKBIT1 ;
STA CLICKBIT2 ;
NEXTNOTE INC SEQUENCE ; increment to next note address
LDA SEQUENCE ; load Accumulator with next note address
BNE ]LOOP ; branch to LOOP
ENDSONG RTS ;

77
ksynth_Output.txt Normal file
View File

@ -0,0 +1,77 @@
------+--------------------+-------------+----+---------+------+-----------------------+-------------------------------------------------------------------
Line | # File Line | Line Type | MX | Reloc | Size | Address Object Code | Source Code
------+--------------------+-------------+----+---------+------+-----------------------+-------------------------------------------------------------------
1 | 1 ksynth.s 1 | Directive | 11 | | 0 | 00/8000 | ORG $0300
2 | 1 ksynth.s 2 | Empty | 11 | | 0 | 00/0300 |
3 | 1 ksynth.s 3 | Equivalence | 11 | | 0 | 00/0300 | DURATION EQU $FA ; Note DURATION
4 | 1 ksynth.s 4 | Equivalence | 11 | | 0 | 00/0300 | XCOUNTER EQU $FB ; X oscillator counter
5 | 1 ksynth.s 5 | Equivalence | 11 | | 0 | 00/0300 | MULTIPLIER EQU $FC ; duration multiplier (defaults to $ff)
6 | 1 ksynth.s 6 | Equivalence | 11 | | 0 | 00/0300 | OSCILLATOR EQU $FD ; second oscillator value - decrements once for vibrato
7 | 1 ksynth.s 7 | Equivalence | 11 | | 0 | 00/0300 | OSCCOUNTER EQU $FE ; tracks second oscillator value
8 | 1 ksynth.s 8 | Equivalence | 11 | | 0 | 00/0300 | SEQUENCE EQU $FF ; note number/address offset from song origin
9 | 1 ksynth.s 9 | Equivalence | 11 | | 0 | 00/0300 | SONGORIGIN EQU $1000 ; song header start address
10 | 1 ksynth.s 10 | Equivalence | 11 | | 0 | 00/0300 | MIDILOOKUP EQU $1100 ; MIDI notes lookup table address
11 | 1 ksynth.s 11 | Equivalence | 11 | | 0 | 00/0300 | CLICKBIT1 EQU $031E ; modified byte - set to FF for rest note
12 | 1 ksynth.s 12 | Equivalence | 11 | | 0 | 00/0300 | CLICKBIT2 EQU $0313
13 | 1 ksynth.s 13 | Empty | 11 | | 0 | 00/0300 |
14 | 1 ksynth.s 14 | Empty | 11 | | 0 | 00/0300 |
15 | 1 ksynth.s 15 | Code | 11 | | 2 | 00/0300 : A5 FD | GENERATOR LDA {$FD} ; load second oscillator value into $FE
16 | 1 ksynth.s 16 | Code | 11 | | 2 | 00/0302 : 85 FE | STA {$FE} ;
17 | 1 ksynth.s 17 | Code | 11 | | 2 | 00/0304 : A9 FF | LDA #$FF ; load duration multiplier into $FC
18 | 1 ksynth.s 18 | Code | 11 | | 2 | 00/0306 : 85 FC | STA {$FC} ;
19 | 1 ksynth.s 19 | Code | 11 | | 2 | 00/0308 : A4 FA | LDY {$FA} ; load duration into Y
20 | 1 ksynth.s 20 | Code | 11 | | 2 | 00/030A : A6 FB | LDX {$FB} ; load oscillator into X
21 | 1 ksynth.s 21 | Code | 11 | | 1 | 00/030C : CA | STARTCOUNT DEX ; start countdown first oscillator, X
22 | 1 ksynth.s 22 | Code | 11 | | 1 | 00/030D : EA | NOP ; wait...
23 | 1 ksynth.s 23 | Code | 11 | | 1 | 00/030E : EA | NOP ;
24 | 1 ksynth.s 24 | Code | 11 | | 2 | 00/030F : D0 05 | BNE COUNTDOWN2 ; if X = 0, click. otherwise, skip
25 | 1 ksynth.s 25 | Code | 11 | | 3 | 00/0311 : 2C 30 C0 | CLICK1 BIT $C030 ; click
26 | 1 ksynth.s 26 | Code | 11 | | 2 | 00/0314 : A6 FB | RESETX LDX {$FB} ; reset X to beginning of countdown
27 | 1 ksynth.s 27 | Code | 11 | | 2 | 00/0316 : C6 FE | COUNTDOWN2 DEC {$FE} ; countdown second oscillator, $FE
28 | 1 ksynth.s 28 | Code | 11 | | 1 | 00/0318 : EA | NOP ; wait...
29 | 1 ksynth.s 29 | Code | 11 | | 1 | 00/0319 : EA | NOP ;
30 | 1 ksynth.s 30 | Code | 11 | | 2 | 00/031A : D0 07 | BNE COUNTTIME ; if $FE = 0, click. otherwise, skip
31 | 1 ksynth.s 31 | Code | 11 | | 3 | 00/031C : 2C 30 C0 | CLICK2 BIT $C030 ; click
32 | 1 ksynth.s 32 | Code | 11 | | 2 | 00/031F : A5 FD | RESETOSC2 LDA {$FD} ; reset $FE to beginning of countdown
33 | 1 ksynth.s 33 | Code | 11 | | 2 | 00/0321 : 85 FE | STA {$FE} ;
34 | 1 ksynth.s 34 | Code | 11 | | 1 | 00/0323 : 88 | COUNTTIME DEY ; countdown duration
35 | 1 ksynth.s 35 | Code | 11 | | 2 | 00/0324 : D0 E6 | BNE STARTCOUNT ; if duration hasn't expired, return and count down oscillators
36 | 1 ksynth.s 36 | Code | 11 | | 2 | 00/0326 : A4 FA | LDY {$FA} ; if duration has expired, reset duration
37 | 1 ksynth.s 37 | Code | 11 | | 2 | 00/0328 : C6 FC | COUNTTIME2 DEC {$FC} ; decrement duration multiplier
38 | 1 ksynth.s 38 | Code | 11 | | 2 | 00/032A : D0 E0 | BNE STARTCOUNT ; if multiplier hasn't expired, return and count down oscillators
39 | 1 ksynth.s 39 | Code | 11 | | 1 | 00/032C : 60 | RTS ; all done
40 | 1 ksynth.s 40 | Empty | 11 | | 0 | 00/032D |
41 | 1 ksynth.s 41 | Directive | 11 | | 0 | 00/032D | ORG $0330
42 | 1 ksynth.s 42 | Empty | 11 | | 0 | 00/0330 |
43 | 1 ksynth.s 43 | Empty | 11 | | 0 | 00/0330 |
44 | 1 ksynth.s 44 | Code | 11 | | 2 | 00/0330 : A9 00 | PLAYER LDA #$00 ; start at zero
45 | 1 ksynth.s 45 | Code | 11 | | 1 | 00/0332 : AA | ozunid_1 TAX ; X = 0 ; LOOP
46 | 1 ksynth.s 46 | Code | 11 | | 3 | 00/0333 : BD 00 10 | LDA {$1000},X ; load note duration
47 | 1 ksynth.s 47 | Code | 11 | | 2 | 00/0336 : F0 35 | BEQ ENDSONG ; if note is 0 duration, end the song
48 | 1 ksynth.s 48 | Code | 11 | | 2 | 00/0338 : 85 FA | STA {$FA} ; store duration at $FA
49 | 1 ksynth.s 49 | Code | 11 | | 1 | 00/033A : E8 | INX ; increment pointer to note value
50 | 1 ksynth.s 50 | Code | 11 | | 3 | 00/033B : BD 00 10 | LDA {$1000},X ; load note MIDI-style value
51 | 1 ksynth.s 51 | Code | 11 | | 1 | 00/033E : A8 | TAY ; lookup note loop value from lookup table
52 | 1 ksynth.s 52 | Code | 11 | | 3 | 00/033F : B9 00 11 | LDA {$1100},Y ;
53 | 1 ksynth.s 53 | Code | 11 | | 2 | 00/0342 : 85 FB | STA {$FB} ; store note value at $FB
54 | 1 ksynth.s 54 | Code | 11 | | 2 | 00/0344 : 85 FD | STA {$FD} ; store note value at $FD
55 | 1 ksynth.s 55 | Code | 11 | | 2 | 00/0346 : C9 FF | CMP #$FF ; if note value is FF, rest
56 | 1 ksynth.s 56 | Code | 11 | | 2 | 00/0348 : D0 06 | BNE SETOSC2 ; skip over if !=FF
57 | 1 ksynth.s 57 | Code | 11 | | 3 | 00/034A : 8D 1E 03 | STA {$031E} ; change the $C030 click to BIT $FF30
58 | 1 ksynth.s 58 | Code | 11 | | 3 | 00/034D : 8D 13 03 | STA {$0313} ; change the $C030 click to BIT $FF30
59 | 1 ksynth.s 59 | Code | 11 | | 2 | 00/0350 : C6 FD | SETOSC2 DEC {$FD} ; decrement $FD for That Karateka Sound™
60 | 1 ksynth.s 60 | Code | 11 | | 1 | 00/0352 : 8A | TXA ; put current note pointer in Accumulator
61 | 1 ksynth.s 61 | Code | 11 | | 2 | 00/0353 : 85 FF | STA {$FF} ; store that in $FF
62 | 1 ksynth.s 62 | Code | 11 | | 3 | 00/0355 : 20 00 03 | JSR GENERATOR ; play the actual note
63 | 1 ksynth.s 63 | Code | 11 | | 3 | 00/0358 : AD 1E 03 | LDA {$031E} ; did we mess with the C030 click?
64 | 1 ksynth.s 64 | Code | 11 | | 2 | 00/035B : C9 FF | CMP #$FF ; if it's FF, we did. change it back.
65 | 1 ksynth.s 65 | Code | 11 | | 2 | 00/035D : D0 08 | BNE NEXTNOTE ; skip if !=FF
66 | 1 ksynth.s 66 | Code | 11 | | 2 | 00/035F : A9 C0 | LDA #$C0 ; set click points back to $c030
67 | 1 ksynth.s 67 | Code | 11 | | 3 | 00/0361 : 8D 1E 03 | STA {$031E} ;
68 | 1 ksynth.s 68 | Code | 11 | | 3 | 00/0364 : 8D 13 03 | STA {$0313} ;
69 | 1 ksynth.s 69 | Code | 11 | | 2 | 00/0367 : E6 FF | NEXTNOTE INC {$FF} ; increment to next note address
70 | 1 ksynth.s 70 | Code | 11 | | 2 | 00/0369 : A5 FF | LDA {$FF} ; load Accumulator with next note address
71 | 1 ksynth.s 71 | Code | 11 | | 2 | 00/036B : D0 C5 | BNE ozunid_1 ; branch to LOOP
72 | 1 ksynth.s 72 | Code | 11 | | 1 | 00/036D : 60 | ENDSONG RTS ;
73 | 1 ksynth.s 73 | Empty | 11 | | 0 | 00/036E |
------+--------------------+-------------+----+---------+------+-----------------------+-------------------------------------------------------------------