I am working on a "simon" game in assembly I need to make a beep sound whenever a button turns on, the beeps should be different from each other as well. thanks
-
1Possible duplicate of [Playing .wav files on DOSBox's Sound Blaster device](http://stackoverflow.com/questions/41359112/playing-wav-files-on-dosboxs-sound-blaster-device) – Paul R May 16 '17 at 09:04
-
1Have you consulted a DOS interrupt reference? – Michael May 16 '17 at 09:16
1 Answers
You can use the speaker to keep your design simple.
The speaker lets you play square waves at different frequencies, it can actually be used to reproduce digital audio but that's more involved.
The speaker is just an electromagnet, when the current flows through it, it is pulled back otherwise it stays in its default position.
By moving the speaker back and forth it's possible to create sound waves.
The speaker can be moved manually or by using the PIT's channel 2.
Bit 0 of port 61h controls the speaker source (0 = manual, 1 = PIT) and the bit 1 of the same port is the "speaker enable" bit when using the PIT (the speaker "position" when not).
Here's a schematic (from this page) missing the manual driving part:
The PIT is controlled via port 40h-43h, we will use Mode 3 (Square Wave generator) setting each time both bytes of the divider.
The PIT has an oscillator running at about 1.193180 MHz, the divider is used to control the period of the square wave.
Without dealing with internals: at each tick of the PIT oscillator the divider loaded is decremented. The period of the square wave is equal to the time needed by the PIT to decrement the divider down to zero.
Producing a sound is just a matter of programming the PIT with the desired divider and enabling the speaker.
At some time later, we need to disable it.
An easy way to do this is using the int 1ch that is called 18.2 times a second.
By saving a duration in a variable when first playing a sound, by decrementing it at each tick of the int 1ch and by disabling the speaker when the count reaches zero it's possible to control the duration of the beep.
Using the int 1ch requires a setup function (beep_setup
) and a teardown function (beep_teardown
).
BITS 16
ORG 100h
__start__:
;Setup
call beep_setup
;Sample beep of ~2sec
mov ax, 2000
mov bx, 36
call beep_play
;Wait for input
xor ax, ax
int 16h
;Tear down
call beep_teardown
mov ax, 4c00h
int 21h
;-------------------------------------------------
;
;Setup the beep ISR
;
beep_setup:
push es
push ax
xor ax, ax
mov es, ax
;Save the original ISR
mov ax, WORD [es: TIMER_INT * 4]
mov WORD [cs:original_timer_isr], ax
mov ax, WORD [es: TIMER_INT * 4 + 2]
mov WORD [cs:original_timer_isr + 2], ax
;Setup the new ISR
cli
mov ax, beep_isr
mov WORD [es: TIMER_INT * 4], ax
mov ax, cs
mov WORD [es: TIMER_INT * 4 + 2], ax
sti
pop ax
pop es
ret
;
;Tear down the beep ISR
;
beep_teardown:
push es
push ax
call beep_stop
xor ax, ax
mov es, ax
;Restore the old ISR
cli
mov ax, WORD [cs:original_timer_isr]
mov WORD [es: TIMER_INT * 4], ax
mov ax, WORD [cs:original_timer_isr + 2]
mov WORD [es: TIMER_INT * 4 + 2], ax
sti
pop ax
pop es
ret
;
;Beep ISR
;
beep_isr:
cmp BYTE [cs:sound_playing], 0
je _bi_end
cmp WORD [cs:sound_counter], 0
je _bi_stop
dec WORD [cs:sound_counter]
jmp _bi_end
_bi_stop:
call beep_stop
_bi_end:
;Chain
jmp FAR [cs:original_timer_isr]
;
;Stop beep
;
beep_stop:
push ax
;Stop the sound
in al, 61h
and al, 0fch ;Clear bit 0 (PIT to speaker) and bit 1 (Speaker enable)
out 61h, al
;Disable countdown
mov BYTE [cs:sound_playing], 0
pop ax
ret
;
;Beep
;
;AX = 1193180 / frequency
;BX = duration in 18.2th of sec
beep_play:
push ax
push dx
mov dx, ax
mov al, 0b6h
out 43h, al
mov ax, dx
out 42h, al
mov al, ah
out 42h, al
;Set the countdown
mov WORD [cs:sound_counter], bx
;Start the sound
in al, 61h
or al, 3h ;Set bit 0 (PIT to speaker) and bit 1 (Speaker enable)
out 61h, al
;Start the countdown
mov BYTE [cs:sound_playing], 1
pop dx
pop ax
ret
;Keep these in the code segment
sound_playing db 0
sound_counter dw 0
original_timer_isr dd 0
TIMER_INT EQU 1ch
Special thanks to Sep Roland for fixing a flaw in the original code!
You can use beep_play
to play a beep, the units used are the "natural" unit of the hardware configuration explained above.
If your frequencies and duration are fixed, these units simplify the code at no cost.
The beep stops after the duration given, you can use beep_stop
to stop it forcefully.
Playing multiple sound at a time it's impossible (even mixing them is impossible without resorting to PWM techniques).
Calling beep_play
while another beep is in play will have the effect of stopping the current beep and starting the new one.

- 41,768
- 5
- 78
- 124
-
An excellent answer. +1 But the *beep_isr* interrupt service routine has an important problem! You can't just use the `AX` register and not have it preserved. The solution is simple if you wrote: `cmp WORD [cs:sound_counter], 0` `je _bi_stop` `dec WORD [cs:sound_counter]`. No need to use `AX` at all. – Sep Roland May 21 '17 at 18:06
-
Thank you very much @SepRoland! I'm updating the answer with credits :) – Margaret Bloom May 21 '17 at 18:27
-
1For future readers wanting to use this in an MBR bootloader or other non-.com case, probably just set up DS to match your `org` and then remove every `cs:`, instead using the default DS segment. Using CS here was just to avoid setting DS I think. – Peter Cordes Aug 07 '21 at 05:10
-
@PeterCordes Yes, exactly. In a bootloader CS is probably equal to DS, so either will do. – Margaret Bloom Aug 07 '21 at 09:02
-
If you want to access static data in a bootloader, you do need to manually set DS. It might be the same as CS, but you don't know whether it's 0 or 0x7C0. (Since you want a segment reg with base=0 to access the IVT, probably just use DS for that as well, with `org 0x7C00`, so everything can use DS. The static storage "variables" will have absolute offsets like 0x7C??, instead of 0) – Peter Cordes Aug 07 '21 at 09:04
-
@PeterCordes I know :) The code is using CS because in an ISR you'd need to set up DS but a COM file has no metadata to handle instructions like `mov ax,
/max ax, @data` (TASM style), so no setup is possible (unless from CS). In a bootloader, it's the same, except, of course, that the programmer needs to set up the segment registers itself. Since it's possible that DS, in both cases, to be set to temporarily point "somewhere else from data" and since the ISR is async, using CS is more robust as long as the assembler is correctly configured to address the vars from CS. – Margaret Bloom Aug 07 '21 at 09:19 -
Oh right, forgot about the fact that the handler runs in an ISR context, not right from the .com or bootloader context. So DS could have been set to something else by some BIOS interrupt handler which was itself interrupted. – Peter Cordes Aug 07 '21 at 09:36
-
Re: DS in a `.com` - AFAIK it's guaranteed that all 4 segment registers are equal on entry to a `.com`, so you can safely use DS without any setup, as long as you use `org 100h` so the assembler uses the right absolute offset. push cs / pop ds would be possible but redundant. – Peter Cordes Aug 07 '21 at 09:38