Beep a tune on the PC speaker
The PC speaker plays one tone at a time. It produces a square wave: a signal that alternates between two levels. Its frequency, measured in hertz (Hz), is the number of complete cycles per second. A higher frequency makes a higher-pitched tone.
This program plays the opening of Beethoven's "Für Elise" from a table. Each pair of dwords holds
a frequency in Hz and a duration in milliseconds. For example, 659, 150 means “play 659 Hz for
150 ms.” A frequency of 0 makes a rest; a duration of 0 ends the tune. The final 0, 0 pair is
the end marker, so the loop checks the duration before playing anything.
Sending a tone and waiting
The example environment provides the speaker at
/dev/input/by-path/platform-pcspkr-event-spkr. Use this path as written: you do not need to find an
event number or create a link. The program opens it for writing and keeps its descriptor in r12.
Like the keyboard events, a speaker command is a 24-byte input_event. Here the program writes
the event to the device. Its type is EV_SND, its code is SND_TONE, and its value is the frequency
in Hz. A value of 0 stops the tone. linux/input.inc provides these constants and field offsets.
tone holds the event in .data. With istruc, each at puts a value at the named field's offset.
The first 16 bytes, the timestamp, stay 0 and are ignored for this command. Before each write, the
loop changes only input_event.value.
The speaker keeps sounding until another command changes it. To hold each note, the program calls
nanosleep with the address of a timespec. The include time.inc defines this 16-byte structure:
tv_sec is an 8-byte count of whole seconds, followed by an 8-byte tv_nsec count of the remaining
nanoseconds. pause: resb timespec_size reserves space for both fields.
Choose Build, then Run, with your sound on. Listen for the pauses between phrases and for the speaker to fall silent at the end.
default rel
%include "sys/syscall.inc"
%include "fcntl.inc"
%include "time.inc"
%include "linux/input.inc"
section .rodata
speaker_path: db "/dev/input/by-path/platform-pcspkr-event-spkr", 0
tune: ; Hz and ms for each note; 0 Hz is a rest, 0 ms the end
dd 659, 150, 622, 150, 659, 150, 622, 150, 659, 150
dd 494, 150, 587, 150, 523, 150, 440, 450, 0, 150
dd 262, 150, 330, 150, 440, 150, 494, 450, 0, 150
dd 330, 150, 415, 150, 494, 150, 523, 450
dd 0, 0
section .data
tone: istruc input_event
at input_event.type, dw EV_SND
at input_event.code, dw SND_TONE
at input_event.value, dd 0 ; the frequency in Hz, 0 for silence
iend
section .bss
pause: resb timespec_size
section .text
global _start
_start:
mov eax, SYS_open
lea rdi, [speaker_path]
mov esi, O_WRONLY
syscall
test rax, rax
js fail
mov r12, rax ; r12: the speaker
lea rbx, [tune] ; rbx: the next note
next_note:
mov eax, [rbx] ; its frequency
mov ecx, [rbx + 4] ; its duration in ms
test ecx, ecx
jz done ; 0 ms: the end of the tune
mov [tone + input_event.value], eax
mov eax, SYS_write
mov rdi, r12
lea rsi, [tone]
mov edx, input_event_size
syscall ; the tone starts, or stops for a rest
mov eax, [rbx + 4] ; wait for the note's duration:
xor edx, edx
mov ecx, 1000
div ecx ; whole seconds in eax, ms left in edx
mov [pause + timespec.tv_sec], rax
imul rdx, rdx, 1000000 ; ms to ns
mov [pause + timespec.tv_nsec], rdx
mov eax, SYS_nanosleep
lea rdi, [pause]
xor esi, esi
syscall
add rbx, 8 ; the next pair of dwords
jmp next_note
done:
mov dword [tone + input_event.value], 0
mov eax, SYS_write
mov rdi, r12
lea rsi, [tone]
mov edx, input_event_size
syscall ; silence
mov eax, SYS_exit
xor edi, edi
syscall
fail:
mov eax, SYS_exit
mov edi, 1
syscall
Following the tune table
rbx points to the next pair. The loop reads its frequency and duration, checks for the end marker,
then writes the frequency to the speaker. After sleeping for the duration, it adds 8 to rbx to
reach the next pair of dwords.
Dividing the duration by 1000 gives whole seconds in eax and the remaining milliseconds in edx.
Multiplying that remainder by 1,000,000 converts it to nanoseconds. For a 150 ms note, the quotient
is 0 and the remainder is 150, so pause holds 0 seconds and 150,000,000 nanoseconds. A 1500 ms note
would instead use 1 second and 500,000,000 nanoseconds.
A rest follows the same path: writing 0 Hz silences the speaker, and sleeping holds that silence.
At the end marker, done writes one last event with value 0 before exiting. The environment also
silences the speaker when the program ends. While stepping, Undo restores the tone from before the
undone instruction, restarting it if it was sounding.
How close is the frequency?
Only frequencies above 20 Hz and below 32,767 Hz sound at all. Within that range, the speaker's timer cannot produce every requested frequency exactly. It runs at 1,193,182 ticks per second and uses a whole number of ticks for each cycle.
For a request of 659 Hz, the timer divides 1,193,182 by 659. The result is about 1810.595, but the divider keeps only the whole part: 1810 ticks per cycle. Dividing 1,193,182 by 1810 gives the actual frequency, about 659.2 Hz. Discarding the fractional part of the divider can make the tone slightly higher than requested. The table still stores the requested frequencies; the program does not need to calculate this timer divider itself.
Try it
- Play it twice as fast: halve every nonzero duration, so 150 ms becomes 75 ms and 450 ms becomes
225 ms. Keep the frequencies and the
0, 0end marker unchanged. Run it and listen for the same pitches in a shorter tune. - Make a siren: replace the tune with
dd 960, 400, 770, 400repeated four times, thendd 0, 0. Run it and listen for the two alternating pitches, each held for 400 ms. - Add a long final note,
dd 440, 1500, just beforedd 0, 0. Step through its duration conversion and check thatpauseholds 1 second and 500,000,000 nanoseconds. Run it and listen for the longer note before the speaker falls silent.