page 58,132
;****************************************************************
;*                                                              *
;*             Digitized Voice Programmer's Toolkit             *
;*             ------------------------------------             *
;*                                                              *
;*                  Sound Playback Primitives                   *
;*                                                              *
;*            Copyright (c) 1991, Farpoint Software             *
;*                                                              *
;****************************************************************

.8086
.MODEL LARGE

;**********************************************************************
;*                                                                    *
;*                             Equates                                *
;*                                                                    *
;**********************************************************************

tccount		equ	72	;reload count to produce 16572 Hz
countmax_18hz	equ	910	;ratio of new timer rate to standard rate

tcaddrc		equ	43h	;timer/counter control register address
tcaddrd0	equ	40h	;timer/counter data register zero (sys clock)
tcaddrd2	equ	42h	;timer/counter data register two (spkr)

tcmode0a	equ	34h	;two-byte mode for system clock timer
tcmode0b	equ	24h	;high-byte mode for system clock timer
tcmode2a	equ	0B0h	;mode for speaker timer (for initialization)

tclatchcmd0	equ	00h	;latch command for reading timer 0
tclatchcmd2	equ	80h	;latch command for reading timer 2

ppiaddr		equ	61h	;programmable peripheral interface address

kbdonly_mask	equ	0FDh	;mask config to allow only keyboard interrupt
intmaskaddr	equ	21h	;address of the interrupt mask register
kbdint_number	equ	9	;vector number of the keyboard hardware int

;CPU speed calibration initialization parameters

init_delayctr		equ	1024
init_cal_increment	equ	512
init_cal_passes		equ	10

;**********************************************************************
;*                                                                    *
;*                         Global variables                           *
;*                                                                    *
;**********************************************************************

.DATA

;CPU speed calibration data

delayctr	dw	init_delayctr-1
cal_increment	dw	init_cal_increment
cal_passes	dw	init_cal_passes

;keyboard hardware interrupt occurrence flag

keyflag		db	0

;interrupt controller mask save byte

original_mask	db	0

;port 61h default configuration

ppibyte		db	0

;**********************************************************************
;*                                                                    *
;*                               Code                                 *
;*                                                                    *
;**********************************************************************

.CODE

	assume	ds:DGROUP

;code-segment storage for keyboard interrupt chain vector

original_kbdint_vec	dd	0

;**********************************************************************
;*          Keyboard Hardware Interrupt Intercept Routine             *
;**********************************************************************

;This routine sets the "keyflag" variable to 1 whenever a keyboard
; interrupt occurs.

kbdint	proc	far

	push	ds
	push	ax
	mov	ax,DGROUP
	mov	ds,ax
	mov	keyflag,1
	pop	ax
	pop	ds
	jmp	dword ptr cs:original_kbdint_vec

kbdint	endp

;**********************************************************************
;*              Calibrated software delay routine                     *
;**********************************************************************

;lots of nops

delay	proc	near

	db	init_delayctr dup (90h)
	ret				;starting delay value
	db	(init_delayctr-1) dup (90h)
	ret				;should never actually be executed

delay	endp

;**********************************************************************
;*         Initialize timer for correct speaker operation.            *
;**********************************************************************

;This procedure returns 0 if successful and 1 if unsuccessful.

set_ppibyte	proc	near

;return value is kept in BX register

	mov	bx,0		;set to 1 later if required

;set gate 2 drive to one

	in	al,ppiaddr
	and	al,0FDh
	or	al,01h
	mov	ppibyte,al
	out	ppiaddr,al

;set timer chip to mode zero (int on terminal count)

	mov	al,tcmode2a
	cli
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2

;set count to 0001h for quick termination

	mov	al,1
	out	tcaddrd2,al
	jmp	short $+2
	jmp	short $+2
	sub	al,al
	out	tcaddrd2,al
	sti
	jmp	short $+2
	jmp	short $+2

;write the standard setup to timer zero

	mov	al,tcmode0a
	cli
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	mov	al,0
	out	tcaddrd0,al
	jmp	short $+2
	jmp	short $+2
	out	tcaddrd0,al
	sti
	jmp	short $+2
	jmp	short $+2

;set timer zero to high-byte mode

	mov	al,tcmode0b
	cli
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	mov	al,0
	out	tcaddrd0,al
	sti
	jmp	short $+2
	jmp	short $+2

;wait for next timer tick

	mov	ah,0
	mov	al,tclatchcmd0
	cli
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd0
	sti
	jmp	short $+2
	jmp	short $+2
	mov	dx,ax
spi_sync:
	mov	al,tclatchcmd0
	cli
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd0
	sti
	jmp	short $+2
	cmp	ax,dx
	mov	dx,ax
	jbe	spi_sync

;Wait for another timer tick (18Hz); this should be
; plenty of time for timer 2 to reach terminal count.

spi_wt1:
	mov	al,tclatchcmd0
	cli
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd0
	sti
	sub	ah,ah
	cmp	ax,dx
	mov	dx,ax
	jbe	spi_wt1

;read the content of timer 2 and check for 0001h

	mov	al,tclatchcmd2
	cli
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd2
	mov	ah,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd2
	sti
	or	al,al		;test high byte for 00h
	jnz	spi_countdone
	cmp	ah,1		;test low byte for 01h
	jnz	spi_countdone

;(arrive here if timer 2 is NOT counting)
;If timer 2 does not count,
; we flag an error and keep going.

	mov	bx,1

;set gate 2 drive to zero (disable counting)

spi_countdone:
	in	al,ppiaddr
	and	al,0FCh
	mov	ppibyte,al
	out	ppiaddr,al

;count is (supposedly) finished; test timer 2 output readback bit

	in	al,ppiaddr
	test	al,20h		;bit 5 should be high
	jnz	spi_done

;wrong polarity, set error flag and continue

	mov	bx,1

spi_done:
	mov	ax,bx
	ret

set_ppibyte	endp

;**********************************************************************
;*                    Set Calibration Constant                        *
;*                                                                    *
;*       This call simply sets the calibration constant to the        *
;*       value specified in the parameter.                            *
;**********************************************************************

;Accepts the following parameters with PASCAL parameter-passing convention:

;Position   Size   Description
;--------   ----   -----------
;   1         2    The calibration constant to be set.

;There is no return value.

psc_parm1	equ	[bp+6]	;length = 2

psc_parmlength	equ	2

	public	PSETCAL

PSETCAL		proc	far

	push	bp
	mov	bp,sp

;in case this routine has been called before, erase the "ret" instruction
; that was placed in the delay routine

	inc	delayctr		;since we decremented it before
	lea	bx,delay
	add	bx,delayctr
	mov	byte ptr cs:[bx],90h	;this is a "nop" instruction

;get the new delay parameter

	mov	ax,psc_parm1
	mov	delayctr,ax

;insert a "ret" instruction at the new location in the delay routine

	lea	bx,delay
	add	bx,delayctr
	mov	byte ptr cs:[bx],0C3h	;this is a "ret" instruction

	dec	delayctr		;for benefit of playback routine

	pop	bp
	ret	psc_parmlength

PSETCAL		endp

;**********************************************************************
;*                      Calibration procedure                         *
;*                                                                    *
;*      This routine must be called once before any calls are made    *
;*      to the playback routine. It measures the CPU execution        *
;*      speed and saves compensation values.                          *
;**********************************************************************

;There are no entry parameters.

;The return value is a double word, defined as follows:

;Return value low word        Meaning
;---------------------        -------
;     0                       success
;     1                       this CPU is too slow to accomplish
;                              normal-speed playback
;     2                       operation not possible under Windows
;                              in "Enhanced" mode
;     3                       unusual speaker drive circuit encountered,
;                              might not output sound

;The return value high word is the actual speed calibration constant
; for this computer

cal_temp1	equ	[bp-2]	;length = 2

cal_locallength	equ	2

	public	PCALIBRATE

PCALIBRATE	proc	far

	push	bp
	mov	bp,sp
	sub	sp,cal_locallength
	push	si
	push	di

;test for the "Enhanced Mode Windows" environment

	mov	ax,1600h
	int	2Fh
	cmp	al,00h
	je	cal_no_enh_win
	cmp	al,80h
	je	cal_no_enh_win
	mov	delayctr,1
	mov	ax,2
	jmp	cal_exit
cal_no_enh_win:

;test speaker drive circuitry and set configuration

	call	set_ppibyte
	mov	cal_temp1,ax

;in case this routine has been called before, erase the "ret" instruction
; that was placed in the delay routine

	inc	delayctr		;since we decremented it before
	lea	bx,delay
	add	bx,delayctr
	mov	byte ptr cs:[bx],90h	;this is a "nop" instruction

;initialize parameters

	mov	delayctr,init_delayctr
	mov	cal_increment,init_cal_increment
	mov	cal_passes,init_cal_passes

;insert a "ret" instruction into the starting location in the delay routine

	lea	bx,delay
	add	bx,delayctr
	mov	byte ptr cs:[bx],0C3h	;this is a "ret" instruction

;no interrupts until we are through calibrating

	cli

;write the standard setup to timer zero

	mov	al,tcmode0a
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	mov	al,0
	out	tcaddrd0,al
	jmp	short $+2
	jmp	short $+2
	out	tcaddrd0,al
	jmp	short $+2
	jmp	short $+2

;set timer zero to high-byte mode

	mov	al,tcmode0b
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	mov	al,0
	out	tcaddrd0,al
	jmp	short $+2
	jmp	short $+2

;initialize loop count

cal_zerocnt:
	sub	cx,cx

;synchronize to timer tick

	mov	ah,0
	mov	al,tclatchcmd0
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd0
	jmp	short $+2
	jmp	short $+2
	mov	si,ax
cal_sync:
	mov	al,tclatchcmd0
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd0
	jmp	short $+2
	cmp	ax,si
	mov	si,ax
	jbe	cal_sync

;calibration loop

cal_dummyloop:
	mov	al,80h
	nop
	nop
	or	ax,ax
	jz	cal_scale
cal_scale:
	sub	ah,ah
	mov	bx,1		;dummy value
	mul	bx
	mov	al,ah
	mov	ah,dl
	lea	bx,delay
	add	bx,ax
	mov	word ptr cs:[bx],9090h	;these are "nop" instructions
	mov	al,ppibyte
	or	al,00h
	out	ppiaddr,al
	and	al,0FFh
	call	delay
	mov	word ptr cs:[bx],9090h	;these are "nop" instructions
	inc	cx
	mov	al,tclatchcmd0
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd0
	sub	ah,ah
	nop
	nop
	cmp	ax,si
	mov	si,ax
	nop
	nop
	jbe	cal_dummyloop

;kill the old "ret" instruction in the delay routine

	lea	bx,delay
	add	bx,delayctr
	mov	byte ptr cs:[bx],90h	;this is a "nop" instruction

;adjust delay counter

	mov	ax,cal_increment
	shr	cal_increment,1
	cmp	cx,countmax_18hz
	je	cal_setret
	jb	cal_makefaster
	add	delayctr,ax
	jmp	cal_setret
cal_makefaster:
	sub	delayctr,ax
cal_setret:				;here we insert a new return
	lea	bx,delay
	add	bx,delayctr
	mov	byte ptr cs:[bx],0C3h	;this is a "ret" instruction
	dec	cal_passes
	cmp	cal_passes,0
	je	cal_restore
	jmp	cal_zerocnt

;repair loss of system timer ticks and restore interrupts

cal_restore:
	mov	bx,40h
	mov	es,bx
	mov	bx,6Ch
	add	word ptr es:[bx],init_cal_passes+1
	adc	word ptr es:[bx+2],0
	cmp	word ptr es:[bx+2],18h
	jb	cal_tc_end
	ja	cal_tc_midnite
	cmp	word ptr es:[bx],0B0h
	jb	cal_tc_end
cal_tc_midnite:
	mov	byte ptr es:[bx+4],1
	sub	word ptr es:[bx],0B0h
	sbb	word ptr es:[bx+2],18h
cal_tc_end:
	sti

;determine if CPU is too slow

	dec	delayctr		;for benefit of playback routine
	cmp	delayctr,64
	jb	cal_cpuslow
	sub	ax,ax
	jmp	cal_exit
cal_cpuslow:
	mov	ax,1
cal_exit:
	or	ax,ax
	jnz	cal_rdc
	cmp	word ptr cal_temp1,0
	jz	cal_rdc
	mov	ax,3
cal_rdc:
	mov	dx,delayctr
	pop	di
	pop	si
	add	sp,cal_locallength
	pop	bp
	ret

PCALIBRATE	endp

;**********************************************************************
;*                        Playback procedure                          *
;**********************************************************************

;Accepts the following parameters with PASCAL parameter-passing convention:

;Position   Size   Description
;--------   ----   -----------
;   1         4    A far pointer to the memory block used for voice data.
;   2         4    A dword indicating the length of the memory block.

;The return value is a dword indicating the number of bytes actually played.

pv_parm1	equ	[bp+10]	;length = 4
pv_parm2_hi	equ	[bp+8]	;length = 2
pv_parm2_lo	equ	[bp+6]	;length = 2

pv_parmlength	equ	8

pv_temp1	equ	[bp-2]	;length = 2
pv_temp2	equ	[bp-4]	;length = 2

pv_locallength	equ	4

	public	PLAYVOICE

PLAYVOICE	proc	far

	push	bp
	mov	bp,sp
	sub	sp,pv_locallength
	push	si
	push	di

;setup speaker and timer 2

	call	set_ppibyte

;mask all interrupts except the keyboard

	cli

	in	al,intmaskaddr
	mov	original_mask,al
	mov	al,kbdonly_mask
	out	intmaskaddr,al

;install the keyboard interrupt intercept routine

	sub	ax,ax
	mov	es,ax
	mov	bx,kbdint_number*4
	mov	ax,es:[bx]
	mov	word ptr cs:original_kbdint_vec,ax
	mov	ax,es:[bx+2]
	mov	word ptr cs:original_kbdint_vec+2,ax
	lea	ax,kbdint
	mov	es:[bx],ax
	mov	ax,cs
	mov	es:[bx+2],ax

	sti

;set up initial conditions

	mov	keyflag,0
	les	si,dword ptr pv_parm1
	mov	cx,pv_parm2_lo
	mov	di,pv_parm2_hi
	jcxz	pv_playloop
	inc	di

;playback loop

pv_playloop:
	mov	al,es:[si]
	inc	si
	jz	pv_incrseg
pv_scale:
	sub	ah,ah
	mov	bx,delayctr
	mul	bx
	mov	al,ah
	mov	ah,dl
	lea	bx,delay
	add	bx,ax
	mov	word ptr cs:[bx],61E6h	;this is an "out 61h,al" instruction
	mov	al,ppibyte
	or	al,02h
	out	ppiaddr,al
	and	al,0FDh
	call	delay
	mov	word ptr cs:[bx],9090h	;these are "nop" instructions
	nop
	mov	al,tclatchcmd0
	out	tcaddrc,al
	jmp	short $+2
	jmp	short $+2
	in	al,tcaddrd0
	sub	ah,ah
	cmp	keyflag,0
	jne	pv_stop
	loop	pv_playloop
	dec	di
	jnz	pv_playloop
pv_stop:
	jmp	short pv_fixkbi

pv_incrseg:
	mov	bx,es
	add	bx,1000h
	mov	es,bx
	jmp	short pv_scale

;remove the keyboard interrupt intercept routine

pv_fixkbi:
	cli

	sub	ax,ax
	mov	es,ax
	mov	bx,kbdint_number*4
	mov	ax,word ptr cs:original_kbdint_vec
	mov	es:[bx],ax
	mov	ax,word ptr cs:original_kbdint_vec+2
	mov	es:[bx+2],ax

;restore the original interrupt mask

	mov	al,original_mask
	out	intmaskaddr,al

	sti

;leave speaker in a known state

	mov	al,ppibyte
	out	ppiaddr,al

;calculate the number of bytes played

	jcxz	pv_calcbytes
	dec	di
pv_calcbytes:
	mov	ax,pv_parm2_lo
	mov	dx,pv_parm2_hi
	sub	ax,cx
	sbb	dx,di
	mov	pv_temp2,ax
	mov	pv_temp1,dx

;compensate for lost timer ticks

	mov	cx,countmax_18hz
	div	cx
	mov	bx,40h
	mov	es,bx
	mov	bx,6Ch
	cli
	add	es:[bx],ax
	adc	word ptr es:[bx+2],0
	cmp	word ptr es:[bx+2],18h
	jb	pv_tc_end
	ja	pv_tc_midnite
	cmp	word ptr es:[bx],0B0h
	jb	pv_tc_end
pv_tc_midnite:
	mov	byte ptr es:[bx+4],1
	sub	word ptr es:[bx],0B0h
	sbb	word ptr es:[bx+2],18h
pv_tc_end:
	sti

;set the return value

	mov	ax,pv_temp2
	mov	dx,pv_temp1

	pop	di
	pop	si
	add	sp,pv_locallength
	pop	bp
	ret	pv_parmlength

PLAYVOICE	endp

	end
