;               Play.asm
;      Emulates Basic's Play commands
; Syntax  PLAY [string[/K] | filespec/F[/K]]
;      /K denotes no keyboard polling
;
CODE SEGMENT                           ;*************************
ASSUME CS:CODE,DS:CODE                 ;*                       *
ORG 100H                               ;*  REMEMBER TO EXE2BIN  *
                                       ;*                       *
START:         JMP    BEGINNING        ;*************************
 
COPYRIGHT      DB     'Copyright l987 Ziff-Davis Publishing Co.',1AH
PROGRAMMER     DB     'Michael J. Mefford'
 
TIMER_TIC      DD     ?
 
OCTAVE         DB     4
LEN            DW     64
TEMPO          DW     136
MUSIC          DB     1
COUNT          DW     0
 
NOTE:
;      C     C#    D     D#    E     F
DW   16744,17736,18792,19912,21096,22352
;      F#    G     G#    A     A#    B
DW   23680,25088,26576,28160,29712,31616
 
FILE           DB     'PLAY.DAT',0
PARA_FLAG      DB     0
FILE_FLAG      DB     0
POLL_KEY       DB     0
 
;----------------------------------------------------------;
; Speed the clock up to four times normal speed.           ;
; Capitalize command line and check for switch characters. ;
;----------------------------------------------------------;
 
BEGINNING:     MOV    BX,16384               ;Divisor for 72.8/sec IRQ 0.
               CALL   SET_CLOCK
 
               MOV    AX,3508H               ;Get INT 8 (timer tic).
               INT    21H
               MOV    WORD PTR TIMER_TIC,BX       ;Save old vector.
               MOV    WORD PTR TIMER_TIC[2],ES
               PUSH   DS
               POP    ES                     ;Restore ES.
 
               MOV    DX,OFFSET INT_8        ;Point INT 8 to our routine.
               MOV    AX,2508H
               INT    21H
 
               CMP    BYTE PTR DS:[80H],0    ;Are there any parameters?
               JZ     PARAMETERS             ;If no, open PLAY.DAT.
 
CAPITALIZE:    MOV    SI,81H                 ;Point to parameters.
NEXT_CAP:      LODSB                         ;Get a byte.
               CMP    AL,13                  ;Is it carriage return?
               JZ     PARAMETERS             ;If yes, done here.
               CMP    AL,'/'                 ;Is it switch character?
               JZ     SWITCHES               ;If yes, check which one.
               CMP    AL,'0'                 ;Is it a possible command?
               JB     NEXT_CAP               ;If no, get next byte.
               MOV    PARA_FLAG,1            ;Else flag that parameter exists.
               CMP    AL,'a'                 ;Is it lower case?
               JB     NEXT_CAP               ;If no, get next byte.
               AND    BYTE PTR [SI-1],5FH    ;Else, capitalize.
               JMP    SHORT NEXT_CAP         ;Next byte.
 
SWITCHES:      CMP    AL,'/'                 ;Is it switch character?
               JNZ    GET_SWITCH             ;If no, get next byte.
               MOV    BYTE PTR [SI-1],0      ;Else create ASCIIZ for DOS.
GET_SWITCH:    LODSB                         ;Get a byte.
               CMP    AL,13                  ;Is it carriage return?
               JZ     PARAMETERS             ;If yes, done here.
               AND    AL,5FH                 ;Else, capitalize.
               CMP    AL,'K'                 ;Is it /K switch?
               JNZ    CK_FILE                ;If no, check /F.
               MOV    POLL_KEY,1             ;Else, flag as no keyboard poll.
CK_FILE:       CMP    AL,'F'                 ;Is it /F?
               JNZ    SWITCHES               ;If no, get next byte.
               MOV    FILE_FLAG,1            ;Else, flag as filespec.
               JMP    SHORT SWITCHES         ;Get next byte.
 
;-----------------------------------------------------------------------;
; Exit is here so it can be reached by as many short jumps as possible. ;
; Before terminating, reset clock to 18.2/sec and restore INT 8 vector. ;
;-----------------------------------------------------------------------;
 
EXIT:          MOV    BX,0                   ;Divisor of 65536 or zero.
               CALL   SET_CLOCK
 
               MOV    DX,WORD PTR TIMER_TIC       ;Restore old vector
               MOV    AX,WORD PTR TIMER_TIC[2]
               MOV    DS,AX
               MOV    AX,2508H                    ; of INT 8.
               INT    21H
 
               INT    20H                    ;Terminate.
 
;-----------------------------------------------------------------;
; Check to see if parameters exist and if yes check if filespec.  ;
; If yes, read file into buffer and process by stripping Wordstar ;
; high bit, capitalizing, stripping comments and stripping space  ;
; characters and below.  Finish by tacking on a carriage return.  ;
;-----------------------------------------------------------------;
 
PARAMETERS:    CMP    PARA_FLAG,1            ;Are there parameter?
               JZ     FILE_NAME              ;If yes, check if filespec.
               MOV    DX,OFFSET FILE         ;Else, point to PLAY.DAT
               JMP    SHORT OPEN_FILE        ; and open file.
 
FILE_NAME:     CMP    FILE_FLAG,1            ;Is it a filespec?
               JNZ    READ_COMMAND           ;If no, read the command line.
               MOV    DX,82H                 ;Else, point to filespec.
 
OPEN_FILE:     MOV    AX,3D00H               ;Open file for reading.
               INT    21H
               JC     EXIT                   ;If not found, exit.
 
               MOV    BX,AX                  ;Else, filehandle in BX.
               MOV    DX,OFFSET BUFFER       ;Read file into buffer.
               MOV    CX,0F000H
               MOV    AH,3FH
               INT    21H
 
               MOV    CX,AX                  ;File length in CX.
               MOV    SI,OFFSET BUFFER       ;Initialized SI and DI
               MOV    DI,OFFSET BUFFER       ; to head of buffer.
 
FORMAT:        LODSB                         ;Get a byte.
               AND    AL,7FH                 ;Strip Wordstar high bit.
               CMP    AL,':'                 ;Is it comment character?
               JZ     STRIP                  ;If yes, strip comment.
               CMP    AL,'a'                 ;Is it lower case?
               JB     CK_CONTROL             ;If no, check space and below.
               AND    AL,5FH                 ;Else, capitalize.
CK_CONTROL:    CMP    AL,32                  ;Is it space or below?
               JBE    NEXT_FORMAT            ;If yes, don't store.
               STOSB                         ;Else store the character.
NEXT_FORMAT:   LOOP   FORMAT                 ;Get next byte.
END_FORMAT:    MOV    BYTE PTR [DI],13       ;Tack on carriage return as EOF.
               MOV    SI,OFFSET BUFFER       ;Point to commands.
               JMP    SHORT NEXT_COMMAND     ;Get the commands.
 
NEXT_STRIP:    LODSB                         ;Get a byte.
               AND    AL,7FH                 ;Strip Wordstar high bit.
               CMP    AL,10                  ;Is it linefeed?
               JZ     NEXT_FORMAT            ;If yes get next command.
STRIP:         LOOP   NEXT_STRIP             ;Else, skip and get next byte.
               JMP    SHORT END_FORMAT
 
;--------------------------------;
; This is the command processor. ;
;--------------------------------;
 
READ_COMMAND:  MOV    SI,82H                 ;Initialize pointer to commands.
 
NEXT_COMMAND:  CMP    POLL_KEY,1             ;Should we poll keyboard?
               JZ     GET_COMMAND            ;If no, skip.
               MOV    AH,1                   ;Else, check for keystroke
               INT    16H                    ; via BIOS.
               JNZ    EXIT                   ;If keystroke, exit.
GET_COMMAND:   LODSB                         ;Else, get a byte.
               CMP    AL,13                  ;Is it carriage return?
               JA     CONTINUE               ;If no, continue.
               JMP    EXIT                   ;Else, we are done.
CONTINUE:      CMP    AL,32                  ;Is it space, comma or semicolon?
               JZ     NEXT_COMMAND           ;If yes, skip.
               CMP    AL,','
               JZ     NEXT_COMMAND
               CMP    AL,';'
               JZ     NEXT_COMMAND
 
               CMP    AL,'O'                 ;Is it "O" ; Octave?
               JNZ    CK_L                   ;If no, check "L".
               CALL   CK_NUMBER              ;Else, get number.
               CMP    AL,7                   ;Is it in range?
               JA     NEXT_COMMAND           ;If no, skip.
               MOV    OCTAVE,AL              ;Else store.
               JMP    SHORT NEXT_COMMAND
 
CK_L:          CMP    AL,'L'                 ;Is it "L" ; Length?
               JNZ    CK_TEMPO               ;If no, check tempo.
               CALL   CK_NUMBER              ;Else, get number.
               CMP    AL,0                   ;Is it between 1 and 64?
               JZ     NEXT_COMMAND           ;If no, skip.
               CMP    AL,64
               JA     NEXT_COMMAND
               XOR    DX,DX                  ;Else, zero in high half.
               MOV    AX,256                 ;Dividend of 256.
               DIV    BX                     ;Divide by length
               MOV    LEN,AX              ; and store.
               JMP    SHORT NEXT_COMMAND
 
CK_TEMPO:      CMP    AL,'T'                 ;Is it "T" ; tempo?
               JNZ    CK_N                   ;If no, check number.
               CALL   CK_NUMBER              ;Else, get number.
               CMP    AL,32                  ;Is it between 32 and 255?
               JB     NEXT_COMMAND           ;If no, skip.
               MOV    AX,255
               SUB    AL,BL                  ;Else, complement.
               INC    AX                     ;Make non zero
               MOV    TEMPO,AX               ; and store.
               JMP    SHORT NEXT_COMMAND
 
CK_N:          CMP    AL,'N'                 ;Is it "N" ; number?
               JNZ    CK_NOTE                ;If no, check note.
               CALL   CK_NUMBER              ;Else, get number.
               CMP    AL,84                  ;Is it between 0 and 84?
               JA     NEXT_COMMAND           ;If no, skip.
               CMP    AL,0
               JZ     GOT_N
               DEC    AL                     ;Adjust number.
GOT_N:         XOR    AH,AH                  ;Zero in high half.
               MOV    CL,12                  ;Divide by 12.
               DIV    CL
               MOV    CX,8                   ;Get octave.
               SUB    CL,AL
               MOV    BL,AH                  ;Remainder in BX pointer.
               XOR    BH,BH
               SHL    BX,1                   ;Adjusts as word pointer.
               MOV    BX,[OFFSET NOTE+BX]    ;Retrieve note.
 
               CALL   PLAY_NUMBER            ;Play the note.
               JMP    NEXT_COMMAND
 
CK_NOTE:       CMP    AL,'A'                 ;Is it letter note ; A-G?
               JB     COMMAND_END
               CMP    AL,'G'
               JA     CK_P                   ;If no, check pause.
               CMP    AL,'C'
               JB     ABOVE_C
               SUB    AL,7                   ;First subtract 7 for C and above.
ABOVE_C:       SUB    AL,60                  ;Finish pointer adjustment.
               XOR    AH,AH                  ;Zero in high half.
               MOV    DI,AX                  ;Move into pointer.
               MOV    CL,2                   ;Convert to word pointer
               SHL    DI,CL                  ; by multiplying by 2.
               CMP    DI,8                   ;Adjust if necessary.
               JBE    GOT_NOTE
               SUB    DI,2
 
GOT_NOTE:      CALL   BLACK_KEY              ;Check for sharps or flats.
               CALL   PLAY                   ;Play the note.
               JMP    NEXT_COMMAND
 
CK_P:          CMP    AL,'P'                 ;Is it "P" ; pause?
               JNZ    CK_K                   ;If no, check key poll.
               CALL   CK_NUMBER              ;Else, get number.
               CMP    AL,0                   ;Is it between 1 and 64?
               JZ     COMMAND_END            ;If no, skip.
               CMP    AL,64
               JA     COMMAND_END
               XOR    DX,DX                  ;Else, zero in high half.
               MOV    AX,256                 ;Dividend of 256.
               DIV    BX                     ;Divide by length
               MOV    BP,AX                  ;Pause in BP.
               CALL   PAUSE_DELAY
               JMP    NEXT_COMMAND
 
CK_K:          CMP    AL,'K'                 ;Is it "K" ; non keyboard poll?
               JNZ    CK_M                   ;If no, check music.
               MOV    POLL_KEY,1             ;Else, flag as no poll.
               JMP    NEXT_COMMAND
 
CK_M:          CMP    AL,'M'                 ;Is it "M" ; music?
               JNZ    COMMAND_END            ;If no, next command.
               LODSB                         ;Else, get next byte.
               CMP    AL,13
               JNZ    CK_N2
               JMP    EXIT
CK_N2:         CMP    AL,'N'                 ;Is it "N" ; normal?
               JNZ    CK_L2                  ;If no, check L.
               MOV    MUSIC,1                ;Else, flag as music one.
CK_L2:         CMP    AL,'L'                 ;Is it "L" ; legato?
               JNZ    CK_S2                  ;If no, check S.
               MOV    MUSIC,2                ;Else, flag as music two.
CK_S2:         CMP    AL,'S'                 ;Is it "S" ; staccato?
               JNZ    COMMAND_END            ;If no, skip.
               MOV    MUSIC,3                ;Else, flag as music three.
COMMAND_END:   JMP    NEXT_COMMAND
 
               ;*************;
               ; Subroutines ;
               ;*************;
 
;---------------------------------------------------------;
; This subroutine converts decimal command number to hex. ;
;---------------------------------------------------------;
 
CK_NUMBER:     XOR    BX,BX                  ;Initialize to zero.
NEXT_NUMBER:   CMP    BYTE PTR [SI],'0'      ;Is it a number?
               JB     END_NUMBER             ;If no, we're done.
               CMP    BYTE PTR [SI],'9'
               JA     END_NUMBER
               LODSB                         ;Get number.
               SUB    AL,30H                 ;Convert to hex.
               MOV    DL,AL
               MOV    AX,10                  ;Shift decimal by ten.
               MUL    BL
               MOV    BL,AL                  ;Result in BL.
               ADD    BL,DL                  ;Add new number.
               JMP    SHORT NEXT_NUMBER      ;Get next number.
 
END_NUMBER:    MOV    AL,BL                  ;Return with number in AL.
               XOR    AH,AH                  ;Zero in high half.
               RET
 
;---------------------------------------------;
; This subroutine checks for sharps or flats. ;
;---------------------------------------------;
 
BLACK_KEY:     CMP    BYTE PTR [SI],'+'      ;Is it sharp?
               JNZ    CK_SHARP               ;If no, check #.
               ADD    DI,2                   ;Else, point to next note.
               INC    SI                     ;Adjust command pointer.
CK_SHARP:      CMP    BYTE PTR [SI],'#'      ;Do same for #.
               JNZ    CK_FLAT
               ADD    DI,2
               INC    SI
CK_FLAT:       CMP    BYTE PTR [SI],'-'      ;Do same for flat except
               JNZ    END_BLACK              ; decrement note pointer.
               SUB    DI,2
               INC    SI
END_BLACK:     RET
 
;---------------------------------;
; This subroutine plays the note. ;
;---------------------------------;
 
PLAY:          MOV    BX,[OFFSET NOTE+DI]    ;Retrieve the note.
               MOV    CX,8                   ;Complement the octave.
               SUB    CL,OCTAVE
PLAY_NUMBER:   SHR    BX,CL                  ;Divide by octave base two.
               MOV    DX,12H                 ;120000h dividend constant.
               XOR    AX,AX
               DIV    BX                     ;Divide by frequency.
               MOV    BX,AX                  ;Store in BX.
               MOV    AL,0B6H                ;Latch to Channel 2.
               OUT    43H,AL
               MOV    AX,BX                  ;Send LSB then MSB to 8253.
               OUT    42H,AL
               MOV    AL,AH
               OUT    42H,AL
               IN     AL,61H
               OR     AL,3                   ;Turn bits 0 and 1 on 8255 chip
               OUT    61H,AL                 ; to turn speaker on.
 
               CALL   DELAY                  ;Delay.
 
OFF:           IN     AL,61H                 ;Turn bits 0 and 1 back off.
               AND    AL,11111100B
               OUT    61H,AL
 
               MOV    CX,BX                  ;Delay for pause between notes.
               CALL   STACCATO
 
               RET
 
;------------------------------------------------;
; This subroutine calculates the length of play. ;
;------------------------------------------------;
 
DELAY:         MOV    BP,LEN                 ;Retrieve length.
PAUSE_DELAY:   MOV    CX,BP                  ;Store in CX.
NEXT_DOT:      CMP    BYTE PTR [SI],'.'      ;Is it a dotted note?
               JNZ    GOT_DELAY              ;If no, got length.
               INC    SI                     ;Else, adjust command pointer.
               SHR    CX,1                   ;Divide length by two and
               ADD    BP,CX                  ; add to total length.
               JMP    SHORT NEXT_DOT         ;Check for another dot.
 
GOT_DELAY:     MOV    CX,BP                  ;Length in CX.
               PUSH   CX                     ;Save.
               CMP    MUSIC,2                ;Is it music legato?
               JZ     WAIT                   ;If yes, play full length.
 
               MOV    AX,CX                  ;Else, length in AX.
               XOR    DX,DX                  ;Zero in high half.
               CMP    MUSIC,1                ;Is it music normal?
               JNZ    MUSIC_3                ;If no, must be music stacatto.
               MOV    CX,7                   ;Multiply by 7/8.
               MUL    CX
               MOV    CL,3
               SHR    AX,CL
               MOV    CX,AX
               JMP    SHORT WAIT
 
MUSIC_3:       MOV    CX,3                   ;Multiply by 3/4.
               MUL    CX
               MOV    CL,2
               SHR    AX,CL
               MOV    CX,AX
 
WAIT:          POP    BX                     ;Recover total length.
               SUB    BX,CX                  ;Subtract pause time.
 
STACCATO:      MOV    AX,TEMPO               ;Retrieve tempo.
               MUL    CL                     ;Multiply by length of note.
               XOR    DX,DX
               MOV    CX,200                 ;Adjust by looping constant.
               DIV    CX
               INC    AX                     ;Prevent zero loop.
 
               MOV    CX,COUNT               ;Get present counter.
               ADD    CX,AX                  ;Add length of note.
               JB     END_DELAY              ;Skip if count wrapped.
NEXT_DELAY:    MOV    AX,COUNT               ;Get counter again.
               CMP    AX,CX                  ;Has delay expired?
               JB     NEXT_DELAY             ;If no, get counter until it does.
END_DELAY:     RET
 
;------------------------------------------------------;
; This subroutine programs Channel 0 of the 8253 chip. ;
;------------------------------------------------------;
 
SET_CLOCK:     MOV    AL,00110110B           ;Latch to Channel 0.
               OUT    43H,AL                 ;Send LSB then MSB.
               MOV    AL,BL
               OUT    40H,AL
               MOV    AL,BH
               OUT    40H,AL
               RET
 
;------------------------------------;
; This is the new INT 8 interceptor. ;
;------------------------------------;
 
ASSUME DS:NOTHING
 
INT_8:         STI                           ;Interrupts back on.
               PUSHF                         ;Save registers.
               PUSH   AX
               PUSH   CX
               PUSH   DX
               INC    COUNT                  ;Increment our counter.
               MOV    AX,COUNT               ;Check if divisible by 4.
               XOR    DX,DX
               MOV    CX,4
               DIV    CX
               CMP    DX,0
               JNZ    TIMER_RET              ;If no, skip clock update.
               POP    DX                     ;Else, restore registers.
               POP    CX
               POP    AX
               POPF
               JMP    TIMER_TIC              ;Pass control to old INT 8.
 
TIMER_RET:     POP    DX                     ;Restore DX and CX; need AX still.
               POP    CX
               CLI                           ;No interrupts.
               MOV    AL,20H                 ;Send 8259 command port EOI
               OUT    20H,AL                 ; end-of-interrupt.
               POP    AX                     ;Restore other registers.
               POPF
               IRET                          ;Interrupt return.
 
BUFFER:
 
CODE ENDS
END  START
 
 
 
