I am new to assembly language
Our computer makes sound all the time (mp3, wav, mp4, ogg, etc).
How could I directly communicate to a speaker with assembly language? In C, you can use "Beep()" and pass duration in milliseconds and hertz of the sound to the speaker. How could I do this in GNU assembly language?
My Computer Specs: Dell Optiplex 960; Ubuntu 23.04 LTS x86-64 architecture.
Could somebody leave and example of how this could be done, I.E. pasting some source code? Leaving comments on relatively what each instruction is doing would also be very helpful.
I have tried to find some tutorials, but am kind of lost. Most tutorials that were in NASM (Not GNU like I use), would use the systems default sound, I.E. the bell sound on windows, or the notification sound on Linux Ubuntu.
Update: According to the online service manual for the Dell Optiplex 960, the internal speaker is an optional device. Therefore, your computer might not have an internal speaker connected and the comments to the question and this answer about using the PC speaker interface will not help much. This answer mainly explains about using the internal speaker, but has a few remarks on PCM audio at the end.
As I have been maintaining the common Linux beep utility https://github.com/spkr-beep/beep for a few years, I would recommend to use the proper API for the PC speaker on Linux without the need for privileged access.
This amounts to a program which does
open(2)
the well-known device file /dev/input/by-path/platform-pcspkr-event-spkr
and save the file descriptor if successful, abort the program otherwisestruct input_event
(defined in linux/input.h
) with e.type = EV_SND
, e.code = SND_TONE
, e.value = frequency_in_Hz
(a frequency of 0
will silence the PC speaker) (can be done statically)write(2)
that 24 byte struct to the file descriptorclose(2)
the file descriptorSo basically, you just need to write a program which writes a few bytes into a file.
If you want your program to start a tone, then wait, and then stop that tone, your program also needs to wait for a defined time. The nanosleep(2)
syscall with a struct timespec
from time.h
can help you there.
This leaves you with just needing to figure out how to execute the syscalls open(2)
, write(2)
and close(2)
. OK, the close(2)
is not really needed if you exit(2)
afterwards anyway, but nanosleep(2)
is probably needed as well.
Any Linux "hello world" program in assembly should be a good starting point: It will already have implemented write(2)
and exit(2)
, and that should be a good place to start from when adding the other syscalls.
Starting with a simple hello world program like https://cs.lmu.edu/~ray/notes/gasexamples/ (first hit in search engine for "linux gnu assembly hello world") with some information from https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ (first hit in search engine for "x86-64 abi") with a few of my own adornments (.size
, foo_size = . - foo
, .balign
and comments, and .global
for debugging) gives the following program which starts a beep. Stopping the beep is left as an exercise to the reader, or for running beep
on the shell.
# gas-beep.s - do a PC speaker beep with GNU assembler for Linux x86-64
.global _start
.text
.global main_program
main_program:
_start:
# open(speaker_device, O_WRONLY)
mov $2, %rax # sys_open
mov $speaker_device, %rdi # filename = speaker_device
mov $1, %rsi # flags = O_WRONLY
xor %rdx, %rdx # mode = 0
syscall
cmp $0, %rax
jl error_exit_open
mov %rax, fd
# write(fd, &beepcmd_on, sizeof(beepcmd_on))
mov $1, %rax # sys_write
mov fd, %rdi # fd = fd
mov $beepcmd_on, %rsi # buf = beepcmd_on
mov $beepcmd_on_size, %rdx # count = sizeof(beepcmd_on)
syscall
# TODO: nanosleep(2) with a struct timeval aka two .quad values
# TODO: turn off sound using write(2) (beepcmd_off with frequency 0)
# TODO: print a few enlightening words to stdout with write(2)
# close(fd)
mov $3, %rax # sys_close
mov fd, %rdi # fd = fd
syscall
# exit(0)
mov $60, %rax # sys_exit
xor %rdi, %rdi # error_code = 0
syscall
error_exit_open:
# write(1, errmsg_open, sizeof(errmsg_open))
mov $1, %rax # sys_write
mov $2, %rdi # fd = STDERR_FILENO
mov $errmsg_open, %rsi # buf = errmsg_open
mov $errmsg_open_size, %rdx # count = sizeof(errmsg_open)
syscall
# exit(2)
mov $60, %rax # sys_exit
xor $2, %rdi # error_code = 2
syscall
.size main_program, . - main_program
.balign 8
.global beepcmd_on
# struct input_event {.type=EV_SND, .code=SND_TONE, .value=880}
beepcmd_on:
.quad 0
.quad 0
.short 0x12 # .type = EV_SND
.short 0x02 # .code = SND_TONE
.int 880 # .value = frequency
beepcmd_on_size = . - beepcmd_on # required for write(2)
.size beepcmd_on, beepcmd_on_size
.global speaker_device
speaker_device:
.asciz "/dev/input/by-path/platform-pcspkr-event-spkr"
.size speaker_device, . - speaker_device
.global errmsg_open
errmsg_open:
.ascii "Error: Could not open(2) the device.\n"
errmsg_open_size = . - errmsg_open # required for write(2)
.size errmsg_open, errmsg_open_size
.bss
.global fd
.lcomm fd, 8 # uninitialized file descriptor variable
To make building this and understanding it easier, use the following GNUmakefile
which builds you a symbol list, a map file, a disassembled view, etc. of the whole program:
.PHONY: all
all: all-local
TARGETS += gas-beep.lss
%.lss: %
objdump -h -S $< > $@
TARGETS += gas-beep.syms
%.syms: %
objdump --syms $< > $@
TARGETS += gas-beep.nm
%.nm: %
nm $< > $@
TARGETS += gas-beep.map
%.map: %
TARGETS += gas-beep
gas-beep: gas-beep.o
ld -g -Map=gas-beep.map -o $@ $^
TARGETS += gas-beep.lst
%.lst: %.o
TARGETS += gas-beep.stripped
%.stripped: %
strip -s -o $@ $<
TARGETS += gas-beep.readelf-a
%.readelf-a: %
readelf -a $< > $@
%.o: %.s
gcc -Wall -Werror -g -Wa,-adhlns=$*.lst -c -o $@ $<
.PHONY: all-local
all-local: $(TARGETS)
.PHONY: clean
clean:
rm -f *.{lss,lst,map,nm,o,readelf-a,stripped,syms} gas-beep
Note that you might need to explicitly install and load the pcspkr.ko
kernel module, and make sure your computer actually has a physical PC speaker connected (speaker or piezo buzzer). Installing your operating system's beep
package could save you some manual sysadmin work.
So much for the PC speaker.
If you want to produce sound via PCM audio instead of using the PC speaker, the API will probably be a lot more complex than just writing a few bytes to a file. Twentry years ago, preparing a simple memory buffer for PCM audio with a square wave to write(2)
to an open(2)
ed PCM audio device /dev/dsp
was of comparable complexity. These days however, even just finding a device to open is a lot more complex, and we are not even talking about buffers running out and refilling buffers, and there is more than one PCM sound API.