.radix 16 org 100 jmp short start intro db 0d db 'Piano! by GeorgeCo.' db 1a notes dw 0eee, 0e18, 0d4e, 0c8e, 0bda, 0b30, 0a8e, 09f8 dw 0968, 08ed, 0862, 07e8, 0777, 070c, 06a7, 0647 start: in al,61 and al,0fc out 61,al xor ax,ax int 16 cmp al,1b jz done in al,60 and al,0f shl al,1 cbw mov bx,ax mov ax,notes+[bx] out 42,al mov al,ah out 42,al in al,61 or al,03 out 61,al mov ah,86 mov cx,0001 mov dx,6000 int 15 jmp short start done: ret
I'd love to walk you through this.
First here's a quote that I'm sure was the inspiration for this app, though I can't remember for sure. It's from my copy of the MASM bible for MASM 5.1
The IBM PC's speaker can be used to generate a tone by programming it via the 8255 chip at port address 61h and using the system timer (Intel 8254 chip) to control the speaker. You first set up the timer as an oscillator by sending the data byte B6h to the port 43h. Then you have to compute the ratio of the frequency of sound you want and the frequency of the timer's clock (1.19 MHz). This value is written to port 42h. The 8255 chip is then told to drive the speaker under the control of the timer. You can do this by reading port 61h and writing the value back with the first two bits set to 1 (performing a bitwise-OR with 3). This gets the sound going. Now you must wait as long as you want the sound to continue and then shut the speaker off by reading port 61h again and setting bots 0 and 0 to zero.
As you will see, this is almost 100% the path I took. I never ended up sending byte B6h to port 43h. I don't know why.
Here we go...
.radix 16 org 100
Assembler boilerplate, setting the
.radix, or default base of bare numbers, to 16 (hexadecimal). Also sets the starting point of the app at 100 (hex, which is 256 decimal). This was the default for .com files. As such, numbers here are assumed to be hex unless specified otherwise (i.e. int 16 is really 0x16h aka 22 decimal)
jmp short start intro db 0d db 'Piano! by GeorgeCo.' db 1a
For some reason I was really into making it so if you were to
type (DOS equivalent of
cat) my program out it would show you what it was in plain English without any weird character codes. My .com files always started in this way, with a jump around a newline (which would move the cursor back to position 0, overwriting the bytecode for the
jmp) and then a quick string describing the app (and sometimes usage info) followed by the
1a, which would cause
type to stop output of the file.
GeorgeCo was what I was going to call my software company which obviously never became a real thing.
notes dw 0eee, 0e18, 0d4e, 0c8e, 0bda, 0b30, 0a8e, 09f8 dw 0968, 08ed, 0862, 07e8, 0777, 070c, 06a7, 0647
These were the precalculated values for the 16 notes (two octaves) I wanted to play. They were based on the a440 pitch standard. As we already know, the numbers were the ratio of the sound you wanted and the frequency of the timer's clock (1.19 MHz).
start: in al,61 and al,0fc out 61,al
Port 61 was the PPI (Programmable Peripheral Interface) port B on PC/XT systems. Bits 1 and 0 were the gate and data controls for the PC speaker (the little speaker inside the tower that usually beeped when you turned it on). Setting these both to 0 ensured that the PC speaker was off.
xor ax,ax int 16
Interrupt 16 was for accessing keyboard services. Calling it with
ah set to
00 called the 'read (wait for) next keystroke' service. Once the interrupt returned
al contained an ascii character. If
al was 00,
ah contained an extended ascii keystroke. In this app, the interrupt was being used for its blocking functionality only, A subsequent
in call was made to get the scan code directly from the keyboard once the interrupt returned.
cmp al,1b jz done
If the key that was pressed was escape, jump to the end of the program (and exit)
in al,60 and al,0f shl al,1 cbw
Port 60 was the PPI port A which was the most recent scan code from they keyboard hardware interrupt. It was then bitmasked with
0f and doubled (because it will be a pointer to an array of words not bytes), using
cbw to expand
al into a signed word in
al could never be less than 10 I have no idea why I didn't just clear ah out instead of using
cbw. I'm sure it was a weird optimization thing I've forgotten.
mov bx,ax mov ax,notes+[bx]
Here we see
ax being used as an offset pointer to the array of numbers in
notes. The scancode we read from the keyboard has now been used to lookup and load one of 16 numbers from that array.
out 42,al mov al,ah out 42,al
To set the frequency of sound you want, you wrote to port
in al,61 or al,03 out 61,al
Turn the speaker back on, now at our frequency selected via the keyboard scan code.
mov ah,86 mov cx,0001 mov dx,6000 int 15
This was the BIOS wait function. It went into an interrupt-enabled waiting period of
cx,dx milliseconds. This was a nice short 'blip' duration I ended up with.
jmp short start
Do it all over again, go back and wait for a keyboard input.
Well I hope you liked this little trip down memory lane. Thanks for reading.