OPLTimer: Introduce new timer for OPL3 and OPL4.

It provides a timing resolution of 1130 Hz, which can also be configured higher.

In theory it would also work on MSX-AUDIO, however that requires more waits to
be added since there must be a 84 / 3.58 µs wait after a data write, and the
interrupt handling up to the int reset write takes less, esp. on faster CPUs.
M CHANGES.md +2 -1
@@ 4,11 4,12 @@ VGMPlay MSX change log
 For the complete list of changes please refer to the
 [revision history](https://hg.sr.ht/~grauw/vgmplay-msx/log).
 
-[1.3] — 2019-??-??
+[1.3] — 2020-??-??
 ------------------
 
   * New chips: YM2203 OPN, YM2608 OPNA, YM2610 OPNB, YM2610B OPNB-B, YM2612 OPN2
   * New sound expansions: Makoto, Neotron, Darky, DalSoRi R2, OPL3 (C0H)
+  * New timer: 1130 Hz if a MoonSound or OPL3 is present
   * Emulation of SN76489 DCSG on PSG
   * Emulation of YM2203 OPN, YM2608 OPNA and YM2612 OPN2 on Yamaha SFG
   * Translation of YM2612 OPN2 frequencies to correct tuning

          
M README.md +2 -1
@@ 26,7 26,8 @@ be manually decompressed to VGM with
 [gunzip for MSX](http://www.grauw.nl/projects/gunzip/) or PC.
 
 The timing resolution is 50 or 60 Hz on MSX1 machines with a TMS9918 VDP, 300 Hz
-on machines with a V9938 or V9958 VDP, and 4000 Hz on MSX turboR.
+on machines with a V9938 or V9958 VDP, 1130 Hz if a MoonSound or OPL3 is
+present, and 4000 Hz on MSX turboR.
 
 For collections of VGM music see:
 

          
M src/drivers/OPL3.asm +6 -2
@@ 25,10 25,12 @@ OPL3: MACRO ?base = OPL3_BASE_PORT, ?nam
 	; a = register
 	; d = value
 	WriteRegister:
+		di
 		out (?base + OPL3_FM_ADDRESS),a
 	basePort: equ $ - 1
-		jp $ + 3     ; wait 32 / 14.32 µs
+		cp (hl)      ; wait 32 / 14.32 µs
 		ld a,d
+		ei
 		out (?base + OPL3_FM_DATA),a
 		ret
 	MaskControl:

          
@@ 43,9 45,11 @@ OPL3: MACRO ?base = OPL3_BASE_PORT, ?nam
 	; a = register
 	; d = value
 	WriteRegister2:
+		di
 		out (?base + OPL3_FM2_ADDRESS),a
-		jp $ + 3     ; wait 32 / 14.32 µs
+		cp (hl)      ; wait 32 / 14.32 µs
 		ld a,d
+		ei
 		out (?base + OPL3_FM2_DATA),a
 		ret
 

          
M src/drivers/OPL4.asm +6 -4
@@ 31,9 31,10 @@ OPL4: MACRO ?fmbase, ?wavebase, ?name
 	; a = register
 	; d = value
 	WriteRegister:
+		di
 		out (?fmbase + OPL4_FM_ADDRESS),a
-		nop     ; wait 56 / 33.87 µs
-		ld a,d
+		ld a,d  ; wait 56 / 33.87 µs
+		ei
 		out (?fmbase + OPL4_FM_DATA),a
 		ret
 	MaskControl:

          
@@ 48,9 49,10 @@ OPL4: MACRO ?fmbase, ?wavebase, ?name
 	; a = register
 	; d = value
 	WriteRegister2:
+		di
 		out (?fmbase + OPL4_FM2_ADDRESS),a
-		nop     ; wait 56 / 33.87 µs
-		ld a,d
+		ld a,d  ; wait 56 / 33.87 µs
+		ei
 		out (?fmbase + OPL4_FM2_DATA),a
 		ret
 

          
A => src/timers/OPLTimer.asm +162 -0
@@ 0,0 1,162 @@ 
+;
+; OPL3/4 interrupt timer
+;
+; 1130 Hz resolution
+;
+; Also works on MSX-AUDIO, but it requires more waits.
+;
+OPLTimer_RATE: equ 11  ; 2: 6214 Hz, 4: 3107 Hz, 11: 1130 Hz
+OPLTimer_SAMPLES: equ 44100 * 288 * OPLTimer_RATE / 3579545
+
+OPLTimer: MACRO
+	super: Timer OPLTimer_Start, OPLTimer_Stop, OPLTimer_Reset, Update
+	tick: equ InterruptHandler.tick
+	basePort: equ InterruptHandler.addressPort
+	lastTick:
+		db 0
+
+	; ix = this
+	; de <- time passed
+	Update: PROC
+		ld b,(ix + OPLTimer.lastTick)
+	Wait:
+		ld a,(ix + OPLTimer.tick)
+		cp b
+		jr z,Wait
+		ld (ix + OPLTimer.lastTick),a
+		sub b
+		ld b,a
+		ld hl,0
+		ld de,OPLTimer_SAMPLES
+	Loop:
+		add hl,de
+		djnz Loop
+		ex de,hl
+		jr super.Callback
+		ENDP
+
+	InterruptHandler: PROC
+		push af
+		ld a,MSXAudio_FLAG_CONTROL
+		out (0),a
+	addressPort: equ $ - 1
+		in a,(0)
+	statusPort: equ $ - 1
+		and 01000000B
+		jr z,OldHook
+		cpl
+		out (1),a
+	dataPort: equ $ - 1
+		ld a,0
+	tick: equ $ - 1
+		inc a
+		ld (tick),a
+	tickReference: equ $ - 2
+		pop af
+		ei
+		ret
+	OldHook:
+		pop af
+	oldHook:
+		ds Interrupt_HOOK_SIZE,0C9H
+		ENDP
+	_size:
+	ENDM
+
+OPLTimer_class: Class OPLTimer, OPLTimer_template, Heap_main
+OPLTimer_template: OPLTimer
+
+; c = base I/O port
+; hl = callback
+; ix = this
+OPLTimer_Construct:
+	call Timer_Construct
+	ld e,ixl
+	ld d,ixh
+	ld hl,OPLTimer.InterruptHandler.tick
+	add hl,de
+	ld (ix + OPLTimer.InterruptHandler.addressPort),c
+	ld (ix + OPLTimer.InterruptHandler.statusPort),c
+	inc c
+	ld (ix + OPLTimer.InterruptHandler.dataPort),c
+	ld (ix + OPLTimer.InterruptHandler.tickReference),l
+	ld (ix + OPLTimer.InterruptHandler.tickReference + 1),h
+	ret
+
+; ix = this
+OPLTimer_Destruct: equ Timer_Destruct
+;	jp Timer_Destruct
+
+; e = register
+; d = value
+; ix = this
+OPLTimer_WriteRegister:
+	ld c,(ix + OPLTimer.basePort)
+	di
+	out (c),e
+	cp (hl)  ; wait 32 / 14.32 µs
+	inc c
+	ei
+	out (c),d
+	ret
+
+; ix = this
+OPLTimer_Start: PROC
+	call OPLTimer_Reset
+	call OPLTimer_InstallInterruptHandler
+	ld de,-OPLTimer_RATE << 8 | MSXAudio_TIMER_1
+	call OPLTimer_WriteRegister
+	ld de,10000000B << 8 | MSXAudio_FLAG_CONTROL
+	call OPLTimer_WriteRegister
+	ld de,00111001B << 8 | MSXAudio_FLAG_CONTROL
+	jp OPLTimer_WriteRegister
+	ENDP
+
+; ix = this
+OPLTimer_Stop:
+	ld de,01111000B << 8 | MSXAudio_FLAG_CONTROL
+	call OPLTimer_WriteRegister
+	jr OPLTimer_UninstallInterruptHandler
+
+; ix = this
+OPLTimer_InstallInterruptHandler:
+	push ix
+	ld ix,Interrupt_instance
+	call Interrupt_Construct
+	pop ix
+	ld c,ixl
+	ld b,ixh
+	ld hl,OPLTimer.InterruptHandler.oldHook
+	add hl,bc
+	ex de,hl
+	ld hl,OPLTimer.InterruptHandler
+	add hl,bc
+	push ix
+	ld ix,Interrupt_instance
+	call Interrupt_Hook
+	pop ix
+	ret
+
+; ix = this
+OPLTimer_UninstallInterruptHandler:
+	push ix
+	ld ix,Interrupt_instance
+	call Interrupt_Destruct
+	pop ix
+	ret
+
+; ix = this
+OPLTimer_Reset:
+	ld a,(ix + OPLTimer.tick)
+	ld (ix + OPLTimer.lastTick),a
+	ret
+
+; f <- c: found
+; c <- base I/O port
+OPLTimer_Detect:
+	ld c,MoonSound_FM_BASE_PORT
+	call OPL3_DetectPort
+	ret c
+	ld c,OPL3_BASE_PORT
+	call OPL3_DetectPort
+	ret

          
M src/timers/Timer.asm +1 -0
@@ 10,6 10,7 @@ 
 	INCLUDE "TimerFactory.asm"
 	INCLUDE "VBlankTimer.asm"
 	INCLUDE "LineTimer.asm"
+	INCLUDE "OPLTimer.asm"
 	INCLUDE "TurboRTimer.asm"
 
 Timer: MACRO ?start, ?stop, ?reset, ?update

          
M src/timers/TimerFactory.asm +46 -0
@@ 6,6 6,8 @@ 
 TimerFactory: MACRO
 	lineTimer:
 		dw 0
+	oplTimer:
+		dw 0
 	vBlankTimer:
 		VBlankTimer
 	vBlankTimerFactory:

          
@@ 22,6 24,7 @@ TimerFactory: MACRO
 ; f <- c: succeeded
 TimerFactory_Create:
 	call TimerFactory_CreateTurboRTimer
+	call nc,TimerFactory_CreateOPLTimer
 	call nc,TimerFactory_CreateLineTimer
 	call nc,TimerFactory_CreateVBlankTimer
 	ret

          
@@ 35,6 38,9 @@ TimerFactory_Destroy:
 	call TimerFactory_DestroyLineTimer
 	pop ix
 	push ix
+	call TimerFactory_DestroyOPLTimer
+	pop ix
+	push ix
 	call TimerFactory_DestroyTurboRTimer
 	pop ix
 	ret

          
@@ 98,6 104,46 @@ TimerFactory_DestroyLineTimer:
 ; ix = this
 ; ix <- timer
 ; f <- c: succeeded
+TimerFactory_CreateOPLTimer:
+	push hl
+	call OPLTimer_Detect
+	pop hl
+	ret nc
+	ld a,(ix + TimerFactory.oplTimer)
+	or (ix + TimerFactory.oplTimer + 1)
+	ret nz
+	push ix
+	call OPLTimer_class.New
+	call OPLTimer_Construct
+	ld e,ixl
+	ld d,ixh
+	ex (sp),ix
+	ld (ix + TimerFactory.oplTimer),e
+	ld (ix + TimerFactory.oplTimer + 1),d
+	pop ix
+	scf
+	ret
+
+; ix = this
+TimerFactory_DestroyOPLTimer:
+	ld e,(ix + TimerFactory.oplTimer)
+	ld d,(ix + TimerFactory.oplTimer + 1)
+	ld a,e
+	or d
+	ret z
+	ld (ix + TimerFactory.oplTimer),0
+	ld (ix + TimerFactory.oplTimer + 1),0
+	ld ixl,e
+	ld ixh,d
+	call OPLTimer_Destruct
+	call OPLTimer_class.Delete
+	and a
+	ret
+
+; hl = callback
+; ix = this
+; ix <- timer
+; f <- c: succeeded
 TimerFactory_CreateTurboRTimer:
 	push hl
 	call TurboRTimer_Detect