IDASM.ASM
1. Add the variables "fasttimecount" and "jerk" at the beginning of the first data segment part and make sure "jerk" is public. I added them before the "drawseg" variable. The code looks like this:
Code: Select all
DATASEG
fasttimecount dw 0
jerk dw 1
PUBLIC jerk
drawseg dw 0
2. Add the following "VWL_WaitVBL" procedure before the "VidRefresh" procedure:
Code: Select all
PROC VWL_WaitVBL NEAR
mov bx, [fasttimecount] ;remember old value
mov dx,STATUS_REGISTER_1
@@waitvbl:
sti ;service interrupts
jmp $+2
cli
in al,dx
test al,00001000b ;look for vertical retrace
jnz @@done
mov ax, [fasttimecount] ;get current value
sub ax, bx ;get difference from old value
cmp ax, 8 ;don't wait longer than 8 full cycles @ 560 Hz
jbe @@waitvbl
@@done:
ret
ENDP
Also note that this is written primarily for VGA cards. VGA cards generally run the 16-color 320x200 pixel graphics mode at 70 Hz, while EGA cards run it at 60 Hz. (Some VGA-based laptops also use 60 Hz for the internal display.) For a 60 Hz display, you would need to wait for 10 cycles of the 560 Hz timer to avoid aborting too soon. But I expect most people to run Keen mods on VGA systems, and waiting too long could also have undesirable side effects. I opted to use the timing that is the best match for 70 Hz, since that's what most people will be using. I have never owned or used an EGA-only PC.
3. Modify the end of the "VidRefresh" procedure (from the "@@setscreen" label to the "ENDP" line) as follows:
Code: Select all
@@setscreen:
mov dx,STATUS_REGISTER_1
; K1n9_Duk3 mod: Simply wait for a display active signal before setting the new
; CRTC start address. Allow interrupts while we are waiting, to avoid missing
; timer interrupts on certain systems.
@@waitnovbl:
sti ;service interrupts
jmp $+2
cli
in al,dx
test al,00001001b
jnz @@waitnovbl
; We should have more than enough time to set a new CRTC start address when the
; display is active, since the address only gets latched at the beginning of a
; VBL, and there will *always* be a sufficiently long delay between the end of
; the last display active signal and the start of the VBL. The only reason why
; some sort of synchronization is required for setting the CRTC start address
; itself is that two separate CRTC registers need to be written for this, which
; can lead to problems if the VBL begins in between the two necessary writes.
; The CRTC latching behavior *should* be identical on all EGA-compatible cards.
;
; set CRTC start
;
mov cx, [screenstart]
mov dx, CRTC_INDEX
mov al, CRTC_STARTHIGH
out dx, al
inc dx
mov al, ch
out dx, al
dec dx
mov al, CRTC_STARTLOW
out dx, al
inc dx
mov al, cl
out dx, al
test [jerk], 1 ; K1n9_Duk3 mod
jnz @@setpan ; K1n9_Duk3 mod
;
; wait for a vertical retrace to set pel panning
;
call VWL_WaitVBL ; K1n9_Duk3 mod
;
; set horizontal panning
;
@@setpan: ; K1n9_Duk3 mod
mov dx,ATR_INDEX
mov al,ATR_PELPAN or 20h
out dx,al
jmp $+2
mov ax,[screenpan]
out dx,al
test [jerk], 1 ; K1n9_Duk3 mod
jz @@done ; K1n9_Duk3 mod
;
; wait for a vertical retrace to avoid flickering
;
call VWL_WaitVBL ; K1n9_Duk3 mod
@@done:
sti ; enable interrupts again
ret
ENDP
Code: Select all
PUBLIC SndPriority,SoundData
AdLibDetected dw 0
imfData dd 0
imfStart dw 0
imfSize dw 0
imfSizeLeft dw 0
imfPlaying dw 0
imfDelay dw 0
imfIntCount dw 0
PUBLIC AdLibDetected
CODESEG
Code: Select all
;========
;
; alOut
; Sends a register/value pair to the OPL chip
; Expects reg in AL, val in AH -- destroys AL and DX
;
;=========
PROC alOut
mov dx, 388h
pushf
cli
out dx, al
rept 6
in al, dx
endm
inc dx
mov al, ah
out dx, al
popf
dec dx
rept 35
in al, dx
endm
ret
ENDP
;=========================================================================
;========
;
; DetectAdLib
; Checks if an AdLib-compatible card is present
; Returns AX = 1 on success, 0 otherwise
;
;=========
PROC DetectAdLib
PUBLIC DetectAdLib
mov ax, 6004h ; reset timers 1 and 2
call alOut
mov ax, 8004h ; clear IRQ flag
call alOut
; If bits 5, 6 and 7 of the status byte aren't all set to 0 after resetting
; the timers and IRQ flags, the AdLib detection has already failed and we
; don't need to try running the rest of the detection code. Most systems
; that don't have an AdLib card will already fail this test, since reading
; the status port will most likely return the value 0xFF.
and al, 0E0h
jnz @@failed
; To check if an AdLib (or compatible) card is present, we need to start
; one of the card's internal timers and wait for it to overflow and
; signal that the timer is done. Timer 1 is the faster one, incrementing
; its counter every 80 microseconds. If we set up the counter value for
; timer 1 to be 1 increment away from an overflow, we only need to wait
; 80 microseconds, otherwise we would need to wait longer.
mov ax, 0FF02h ; Set timer 1 counter to largest possible value
call alOut
mov ax, 2104h ; Start timer 1
call alOut
; Now we need to wait AT LEAST 80 microseconds before it's safe to say
; that no AdLib or compatible sound card is present. It won't hurt to
; wait a little longer, though. The AdLib detection was successful if we
; read a status byte that has bits 6 and 7 set to 1 (bit 7 means one of
; the timers has overflowed if it's set, and bit 6, if set, means timer
; 1 has overflowed) and bit 5 set to 0 (bit 5 is the timer 2 status bit
; and we disabled timer 2 at the beginning and cleared the status bits,
; so that bit shouldn't be set to 1 now).
;
; So we basically just need to wait until we read a status byte that has
; the bits set in the correct combination.
;
; To avoid waiting forever when no AdLib card is present, we need some
; method of checking how much time has elapsed. I will use the 18.2 Hz
; BIOS timer for that, just to erase any dependencies on other interrupt
; handlers. This timer gets increased roughly every 55 milliseconds, which
; is MUCH longer than the 80 microseconds that we need to wait, but that
; doesn't really matter because most systems that don't have an AdLib
; (-compatible) sound card will already fail the first test above.
; Since we can never be sure how close we were to the next BIOS timer
; interrupt when we started the AdLib's timer, we need to wait for at
; least two BIOS timer interrupts to avoid aborting too soon.
mov ax, 40h
mov es, ax
mov bx, [es:6Ch] ; read BIOS timer value
@@waitloop:
in al, dx ; read status bits
and al, 0E0h
cmp al, 0C0h ; if timer 1 is done
je @@success
mov ax, [es:6Ch] ; read BIOS timer value
sub ax, bx ; calculate number of 18.2 Hz ticks elapsed
cmp ax, 2 ; AdLib detection fails after 2 tics
jb @@waitloop
@@failed:
xor ax, ax ; AdLib detection failed
ret
; When we get here, the AdLib detection was successful. Time to turn off
; the AdLib's internal timers again:
@@success:
mov ax, 6004h ; reset timers 1 and 2
call alOut
mov ax, 8004h ; clear IRQ flag
call alOut
; And now we need to put the AdLib into a well-defined state by setting
; all registers to 0:
mov cx, 0F4h
@@initloop:
mov ax, cx
call alOut
loop @@initloop
; Note: Many IMF music tracks depend on Waveform Selection (WSE) being
; enabled, but don't contain the necessary instruction to enable it,
; which is why the next alOut call is necessary. Some sound effects may
; depend on this as well.
mov ax, 2001h ; Set WSE=1
call alOut
mov ax, 1 ; AdLib detection was successful
ret
ENDP
;=========================================================================
;========
;
; PauseMusic
; Pauses music playback and turns all notes off
;
;=========
PROC PauseMusic
PUBLIC PauseMusic
mov [imfPlaying], 0
mov ax, 00BDh
call alOut
mov cx, 00B0h
@@noteoff:
mov ax, cx
call alOut
inc cx
cmp cx, 00B9h
jbe @@noteoff
ret
ENDP
;=========================================================================
;========
;
; PlayMusic
; Starts playback of a block of music data
;
;=========
PROC PlayMusic data:DWORD, len:WORD
PUBLIC PlayMusic
cmp [AdLibDetected], 0
jz @@done
call PauseMusic
mov ax, [len]
shr ax, 1
shr ax, 1
mov [imfSize], ax
mov [imfSizeLeft], ax
mov ax, [WORD data]
mov dx, [WORD data+2]
mov [WORD imfData+2], dx
mov [WORD imfData], ax
mov [imfStart], ax
mov [imfDelay], 0
mov [imfPlaying], 1
@@done:
ret
ENDP
;=========================================================================
;========
;
; CleanAdLib
; Totally shuts down the AdLib card
;
;=========
PROC CleanAdLib
PUBLIC CleanAdLib
cmp [AdLibDetected], 0
jz @@done
mov [imfPlaying], 0
mov cx, 0F4h
@@initloop:
mov ax, cx
call alOut
loop @@initloop
@@done:
ret
ENDP
;=========================================================================
;========
;
; UpdateIMF
; interrupt handler for IMF music playback
;
;=========
IMF_TIMER = 852h ; timer constant for 560 Hz
SPK_TIMER = 2000h ; timer constant that UpdateSPKR expects (about 145 Hz)
PROC UpdateIMF FAR
PUBLIC UpdateIMF
push ax
push dx
push si
push ds
push es
cld
mov ax, @Data
mov ds, ax
cmp [imfPlaying], 0
jz @@timing
cmp [imfDelay], 0
jnz @@next
les si, [imfData]
@@dataloop:
lods [WORD es:si] ; read reg/val pair
call alOut
lods [WORD es:si] ; read delay value
dec [imfSizeLeft]
jz @@ended
add [imfDelay], ax
jz @@dataloop
mov [WORD imfData], si
@@next:
dec [imfDelay]
jmp short @@timing
@@ended:
mov ax, [imfStart]
mov [WORD imfData], ax
mov ax, [imfSize]
mov [imfSizeLeft], ax
mov [imfDelay], 0
@@timing:
inc [fasttimecount]
add [imfIntCount], IMF_TIMER
cmp [imfIntCount], SPK_TIMER
jb @@ack
sub [imfIntCount], SPK_TIMER
pushf
call FAR PTR UpdateSPKR
jmp short @@done
@@ack:
mov al, 20h
out 20h, al
@@done:
pop es
pop ds
pop si
pop dx
pop ax
iret
ENDP
;=========================================================================
Code: Select all
PROC StartupSound
PUBLIC StartupSound
test [dontplay],0ffffh
je @@dowork
ret
@@dowork:
test [SPKactive],0FFFFh ;see if library is active
jne @@started ;library was allready started
@@start:
call NEAR PTR StopSound ;make sure nothing is playing
mov ax,3508h ;call bios to get int 8
int 21h
mov [WORD oldint8],bx
mov ax,es
mov [WORD oldint8+2],ax
mov ax, 8
mov [intcount], al
; ALWAYS hook up UpdateIMF instead of UpdateSPKR as the int 8 handler:
call near ptr DetectAdLib
mov [AdLibDetected], ax
push ds
push cs
pop ds
lea dx,[UpdateIMF]
mov ax,2508h ;call bios to set int 8
int 21h
pop ds
;mov bx,2000h
mov bx, IMF_TIMER ; run UpdateIMF at 560 Hz
cli
mov al,36h ;tell the timer chip we are going to
out 43h,al ;change the speed of timer 0
mov al,0
mov al,bl
out 40h,al ;low
mov al,bh
out 40h,al ;high
sti
inc [SPKactive] ;sound routines are now active
@@started:
mov ax,1
mov [soundmode],ax ;set soundmode to SPKR
ret
ENDP
Code: Select all
PROC ShutdownSound
PUBLIC ShutdownSound
test [dontplay],0ffffh
je @@dowork
ret
@@dowork:
call CleanAdLib ; K1n9_Duk3 mod
cli
mov al,36h ;tell the timer chip we are going to
out 43h,al ;change the speed of timer 0
mov al,0 ;system expects 0000 for rate
out 40h,al ;low
out 40h,al ;high
sti
For the most basic music playback, you need the following variable and two C functions to start and stop music playback:
Code: Select all
static Uint16 lastmusic = ~0;
static void far *musicalloc;
void StopMusic(void)
{
PauseMusic();
lastmusic = ~0;
if (musicalloc)
{
farfree(musicalloc);
}
}
void StartMusic(Uint16 num)
{
char musicfile[16];
Uint16 far *buffer;
if (AdLibDetected && num != lastmusic)
{
StopMusic();
itoa(num, musicfile, 10);
strcat(musicfile, ".imf");
buffer = (Uint16 far *)bloadin(musicfile);
if (buffer)
{
musicalloc = lastparalloc; // save it so that the music can be freed correctly later
// Note: This is for playing "Type 1" IMF files. The first two bytes
// of these files contain the size of the IMF data.
PlayMusic(buffer+1, *buffer);
lastmusic = num;
}
}
}
I designed the code so that calling StartMusic will do nothing if the program is already playing the music that StartMusic would have started. This is necessary for the default use case I had in mind (see below). If you want to force the code to restart the music even if the desired music is already playing, you can call StopMusic before calling StartMusic.
IDLIB.H
And to make all of this functionality safely available to the rest of the code, you need to insert the following prototypes and extern declarations into the appropriate header file (IDLIB.H, after the other sound-related declarations, for example):
Code: Select all
void PauseMusic(void);
void PlayMusic(void far *data, Uint16 size);
extern Uint16 AdLibDetected;
void StopMusic(void);
void StartMusic(Uint16 num);
Now you have the tools, but you won't hear any music play unless you add StartMusic calls to your game code. One relatively easy way to do that would be to modify the ReadLevel function in KEENMAIN.C and make it load the music based on the current level number. All you would have to do is to insert the line
Code: Select all
StartMusic(number);
This use case is why I designed StartMusic to do nothing when the desired music number is already being played. Since the DrawPicFile function corrupts the level data, some levels like the menu/intro level and the ending sequence level need to be re-loaded several times, which would also cause the StartMusic function to be called again. You probably don't want the music to restart in that case.
By the way, this is pretty similar to what TSRMUSIC does. It also monitors the level number variable and loads a new piece of music when that number changes. The main difference is that this implementation uses a fixed naming scheme ("0.imf", "1.imf", ... "90.imf") for the music files instead of using a music list file. This means that if you intend to use the same music for several levels, you would need to have several copies of the same music file in your game directory. This can waste quite a bit of drive space, but that aspect should be negligable for most mods. If you need to, you can always write your own code to make things more flexible. I was trying to keep the code small and simple to leave enough space for whatever else you want to add to the code in your mod.
Another difference between this code and TSRMUSIC is that this uses dynamic memory allocation. That means the music can be up to 64 kilobytes in size (TSRMUSIC only supports up to 32 kilobytes), but the downside is that the game will crash with an "Out of memory!" error if there is not enough memory available to load the music file into memory. There are ways to avoid crashes, but I was trying to keep the code small.
Keep in mind that you don't have to implement it like this. You are free to place your StartMusic and StopMusic calls elsewhere in the code. There might be much better ways to handle this, depending on what you are trying to achieve.
Fix Jerky Motion:
Let's get back to the "jerk" variable that we added and made public in step 1, but haven't actually done anything with outside the assembly code yet. If you want to give users of actual DOS machines the option to switch between the two methods of applying the panning value, you need to add a parameter check in your "main" function in KEENMAIN.C and let it set the "jerk" variable to 0 when a certain parameter is used.
But to access that variable, you need an extern declaration for it first. You can put it in a header file (like IDLIB.H) if you want, but you can also add it right before the variable is accessed to keep it simple if the variable isn't going to be used anywhere else in the C code.
I would add the parameter check in between the video card type check and loading the graphics, mainly because I thought it would make sense to have the associated text message show up after the EGA/VGA "card detected" message. It could look like this:
Code: Select all
for (i=1; i<argc; i++)
{
if (stricmp(argv[i], "/JERK") == 0)
{
extern Sint16 jerk;
puts("Jerky motion fix active");
jerk = 0;
break;
}
}
